From 61cf5d8062682a53a3b8cbf38aec4b846419f757 Mon Sep 17 00:00:00 2001 From: sergiyv-improving <69876824+sergiyv-improving@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:10:54 -0800 Subject: [PATCH] upstream (#39) * Configuration Profiles (#711) Co-authored-by: sergiyvamz * chore(deps): bump org.testcontainers:postgresql from 1.19.1 to 1.19.2 (#743) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.vertx:vertx-stack-depchain from 4.4.6 to 4.5.0 (#745) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.testcontainers:junit-jupiter from 1.19.1 to 1.19.2 (#747) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-api from 1.31.0 to 1.32.0 (#746) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump com.fasterxml.jackson.core:jackson-databind from 2.15.3 to 2.16.0 (#744) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Disable failing integration test for PG driver (#742) * Configuration Profiles documentation (#738) * feat: Autoregister a target driver (#748) * chore: reduce log level for intentionally ignored exceptions (#751) * chore(deps): bump org.testcontainers:mariadb from 1.19.1 to 1.19.3 (#756) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:secretsmanager from 2.21.21 to 2.21.31 (#762) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-sdk from 1.31.0 to 1.32.0 (#758) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.testcontainers:postgresql from 1.19.2 to 1.19.3 (#757) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.testcontainers:junit-jupiter from 1.19.2 to 1.19.3 (#759) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: node fastest response time strategy (#755) * chore: update changelog and versioning for version 2.3.1 (#754) * chore(deps): bump org.testcontainers:testcontainers from 1.19.1 to 1.19.3 (#771) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.mariadb.jdbc:mariadb-java-client from 3.3.0 to 3.3.1 (#767) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.apache.poi:poi-ooxml from 5.2.4 to 5.2.5 (#769) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:secretsmanager from 2.21.31 to 2.21.38 (#772) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:rds from 2.21.11 to 2.21.38 (#773) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:rds from 2.21.38 to 2.21.42 (#776) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.postgresql:postgresql from 42.6.0 to 42.7.1 (#778) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:secretsmanager from 2.21.38 to 2.21.43 (#781) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-exporter-otlp from 1.32.0 to 1.33.0 (#777) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: use existing entries to update the round robin cache (#739) * set hostId in HostSpec (#782) * docs: update HikariCP example to include configuring the datasource with a JDBC URL (#749) * Enhanced host monitoring plugin ver.2 (#764) * Fix: expose AuroraInitialConnectionStrategyPlugin with a plugin code (#784) * feat: FederatedAuthConnectionPlugin (#741) * chore: replace synchronized with locks in AwsCredentialsManager (#785) * docs: FederatedAuthPlugin (#787) Co-authored-by: Karen <64801825+karenc-bq@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-sdk-metrics from 1.32.0 to 1.33.0 (#792) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:ec2 from 2.21.12 to 2.22.1 (#795) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-api from 1.32.0 to 1.33.0 (#794) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.junit.jupiter:junit-jupiter-params from 5.10.0 to 5.10.1 (#793) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Improve efm2 failure detection timing (#797) * chore: update versioning and changelog (#791) * fix: SqlMethodAnalyzer to handle empty SQL query and not throw IndexOutOfBoundsException (#798) * Add documentation for read/write splitting Spring limitations (#800) * Add example code for Read/Write Splitting sample (#765) * fix: restructuring try blocks in dialects for exception handling (#799) * chore(deps): bump software.amazon.awssdk:secretsmanager from 2.21.43 to 2.22.5 (#802) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.vertx:vertx-stack-depchain from 4.5.0 to 4.5.1 (#803) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump com.amazonaws:aws-xray-recorder-sdk-core from 2.14.0 to 2.15.0 (#804) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: add missing log message (#801) Co-authored-by: Bruno Paiva Lima da Silva <64107800+brunos-bq@users.noreply.github.com> * fix: making a variable volatile in RdsHostListProvider (#806) * chore(deps): bump software.amazon.awssdk:ec2 from 2.22.1 to 2.22.9 (#808) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump io.opentelemetry:opentelemetry-sdk from 1.32.0 to 1.33.0 (#809) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump org.postgresql:postgresql from 42.6.0 to 42.7.1 (#810) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump tj-actions/changed-files from 37 to 41 in /.github/workflows (#811) * transfer session state during failover (#814) * feat: Session state transfer redesign (#821) * chore(deps): bump software.amazon.awssdk:rds from 2.21.42 to 2.22.13 (#822) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump software.amazon.awssdk:sts from 2.21.42 to 2.22.13 (#823) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump com.fasterxml.jackson.core:jackson-databind from 2.16.0 to 2.16.1 (#818) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump com.github.spotbugs from 5.2.+ to 6.0.6 (#820) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Improve Multi-AZ cluster detection (#824) --------- Signed-off-by: dependabot[bot] Co-authored-by: Bruno Paiva Lima da Silva <64107800+brunos-bq@users.noreply.github.com> Co-authored-by: sergiyvamz Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sergiyvamz <75754709+sergiyvamz@users.noreply.github.com> Co-authored-by: Karen <64801825+karenc-bq@users.noreply.github.com> Co-authored-by: crystall-bitquill <97126568+crystall-bitquill@users.noreply.github.com> Co-authored-by: aaronchung-bitquill <118320132+aaronchung-bitquill@users.noreply.github.com> Co-authored-by: congoamz <75754763+congoamz@users.noreply.github.com> --- .github/workflows/code_quality.yml | 2 +- .github/workflows/main.yml | 2 +- .../workflows/mysql_advanced_performance.yml | 55 ++ .github/workflows/mysql_performance.yml | 6 +- .github/workflows/pg_advanced_performance.yml | 55 ++ .github/workflows/pg_performance.yml | 6 +- .github/workflows/run-autoscaling-tests.yml | 2 +- .github/workflows/run-hibernate-orm-tests.yml | 2 +- .github/workflows/run-integration-tests.yml | 2 +- .../run-standard-integration-tests.yml | 2 +- CHANGELOG.md | 35 +- Maintenance.md | 45 +- README.md | 57 +- benchmarks/README.md | 2 +- benchmarks/build.gradle.kts | 4 +- .../ConnectionPluginManagerBenchmarks.java | 37 +- .../jdbc/benchmarks/PluginBenchmarks.java | 27 +- config/checkstyle/checkstyle-suppressions.xml | 6 +- docs/GettingStarted.md | 12 +- docs/files/configuration-profile-presets.pdf | Bin 0 -> 141176 bytes docs/images/configuration-presets.png | Bin 0 -> 135055 bytes .../session_state_switch_connection.jpg | Bin 0 -> 59399 bytes .../ConfigurationPresets.md | 42 ++ docs/using-the-jdbc-driver/DataSource.md | 5 +- docs/using-the-jdbc-driver/SessionState.md | 42 ++ .../UsingTheJdbcDriver.md | 110 ++-- .../using-plugins/UsingTheFailoverPlugin.md | 2 +- .../UsingTheFederatedAuthPlugin.md | 49 ++ .../UsingTheHostMonitoringPlugin.md | 18 + .../UsingTheReadWriteSplittingPlugin.md | 86 ++- examples/AWSDriverExample/build.gradle.kts | 17 +- .../amazon/FederatedAuthPluginExample.java | 53 ++ examples/HikariExample/build.gradle.kts | 2 +- .../java/software/amazon/HikariExample.java | 6 + .../amazon/HikariFailoverExample.java | 6 + .../ReadWriteSplittingSample/build.gradle.kts | 26 + .../gradle.properties | 16 + .../amazon/ReadWriteSplittingSample.java | 267 ++++++++ .../src/main/resources/logging.properties | 23 + examples/SpringBootHikariExample/README.md | 2 +- .../SpringBootHikariExample/build.gradle.kts | 2 +- examples/SpringHibernateExample/README.md | 2 +- .../SpringHibernateExample/build.gradle.kts | 4 +- examples/SpringTxFailoverExample/README.md | 2 +- examples/SpringWildflyExample/README.md | 6 +- .../spring/build.gradle.kts | 4 +- .../software/amazon/jdbc/main/module.xml | 2 +- examples/VertxExample/README.md | 2 +- examples/VertxExample/build.gradle.kts | 6 +- gradle.properties | 2 +- settings.gradle.kts | 6 +- wrapper/build.gradle.kts | 155 ++++- .../jdbc/ConnectionPluginChainBuilder.java | 46 +- .../amazon/jdbc/ConnectionPluginManager.java | 38 +- .../amazon/jdbc/ConnectionProvider.java | 6 +- .../jdbc/DataSourceConnectionProvider.java | 12 +- .../java/software/amazon/jdbc/Driver.java | 108 ++- .../amazon/jdbc/DriverConnectionProvider.java | 13 +- .../jdbc/HikariPooledConnectionProvider.java | 55 +- .../java/software/amazon/jdbc/HostSpec.java | 26 +- .../software/amazon/jdbc/HostSpecBuilder.java | 12 +- .../amazon/jdbc/PluginManagerService.java | 2 - .../software/amazon/jdbc/PluginService.java | 35 +- .../amazon/jdbc/PluginServiceImpl.java | 218 ++++--- .../amazon/jdbc/PropertyDefinition.java | 51 ++ .../amazon/jdbc/RoundRobinHostSelector.java | 11 +- .../amazon/jdbc/TargetDriverHelper.java | 85 +++ .../authentication/AwsCredentialsManager.java | 43 +- .../jdbc/dialect/AuroraMysqlDialect.java | 22 +- .../amazon/jdbc/dialect/AuroraPgDialect.java | 69 +- .../amazon/jdbc/dialect/DialectManager.java | 7 +- .../amazon/jdbc/dialect/MariaDbDialect.java | 22 +- .../amazon/jdbc/dialect/MysqlDialect.java | 22 +- .../amazon/jdbc/dialect/PgDialect.java | 26 +- .../RdsMultiAzDbClusterMysqlDialect.java | 38 +- .../dialect/RdsMultiAzDbClusterPgDialect.java | 38 +- .../amazon/jdbc/dialect/RdsMysqlDialect.java | 23 +- .../amazon/jdbc/dialect/RdsPgDialect.java | 34 +- .../amazon/jdbc/ds/AwsWrapperDataSource.java | 88 ++- .../hostlistprovider/RdsHostListProvider.java | 4 +- .../jdbc/plugin/AbstractConnectionPlugin.java | 2 +- .../plugin/AuroraConnectionTrackerPlugin.java | 36 +- ...AuroraHostListConnectionPluginFactory.java | 1 + ...AuroraInitialConnectionStrategyPlugin.java | 405 ++++++++++++ ...nitialConnectionStrategyPluginFactory.java | 29 + .../jdbc/plugin/DefaultConnectionPlugin.java | 45 +- .../jdbc/plugin/IamAuthConnectionPlugin.java | 56 +- .../jdbc/plugin/OpenedConnectionTracker.java | 2 +- .../amazon/jdbc/plugin/TokenInfo.java | 41 ++ .../efm/HostMonitoringConnectionPlugin.java | 8 +- .../amazon/jdbc/plugin/efm/MonitorImpl.java | 8 +- .../efm2/HostMonitoringConnectionPlugin.java | 285 ++++++++ ...HostMonitoringConnectionPluginFactory.java | 30 + .../amazon/jdbc/plugin/efm2/Monitor.java | 28 + .../plugin/efm2/MonitorConnectionContext.java | 67 ++ .../amazon/jdbc/plugin/efm2/MonitorImpl.java | 420 ++++++++++++ .../jdbc/plugin/efm2/MonitorInitializer.java | 33 + .../jdbc/plugin/efm2/MonitorService.java | 46 ++ .../jdbc/plugin/efm2/MonitorServiceImpl.java | 185 ++++++ .../ClusterAwareWriterFailoverHandler.java | 1 - .../failover/FailoverConnectionPlugin.java | 157 +---- .../AdfsCredentialsProviderFactory.java | 252 +++++++ .../CredentialsProviderFactory.java | 29 + .../federatedauth/FederatedAuthPlugin.java | 319 +++++++++ .../FederatedAuthPluginFactory.java | 55 ++ .../federatedauth/HttpClientFactory.java | 71 ++ .../NonValidatingSSLSocketFactory.java | 98 +++ .../SamlCredentialsProviderFactory.java | 60 ++ .../ReadWriteSplittingPlugin.java | 59 +- .../FastestResponseStrategyPlugin.java | 208 ++++++ .../FastestResponseStrategyPluginFactory.java | 30 + .../HostResponseTimeService.java | 39 ++ .../HostResponseTimeServiceImpl.java | 112 ++++ .../NodeResponseTimeMonitor.java | 234 +++++++ .../jdbc/profile/ConfigurationProfile.java | 193 ++++++ .../profile/ConfigurationProfileBuilder.java | 136 ++++ .../ConfigurationProfilePresetCodes.java | 74 +++ .../profile/DriverConfigurationProfiles.java | 493 +++++++++++++- ... => ResetSessionStateOnCloseCallable.java} | 19 +- .../states/RestoreSessionStateCallable.java | 42 -- .../amazon/jdbc/states/SessionState.java | 52 ++ .../amazon/jdbc/states/SessionStateField.java | 92 +++ .../jdbc/states/SessionStateHelper.java | 99 --- .../jdbc/states/SessionStateService.java | 102 +++ .../jdbc/states/SessionStateServiceImpl.java | 440 +++++++++++++ .../states/SessionStateTransferCallable.java | 45 -- .../TransferSessionStateOnSwitchCallable.java | 26 + .../GenericTargetDriverDialect.java | 18 + ...ceHelper.java => MariadbDriverHelper.java} | 37 +- .../MariadbTargetDriverDialect.java | 21 +- ....java => MysqlConnectorJDriverHelper.java} | 37 +- .../MysqlConnectorJTargetDriverDialect.java | 53 +- ...aSourceHelper.java => PgDriverHelper.java} | 33 +- .../PgTargetDriverDialect.java | 59 +- .../TargetDriverDialect.java | 16 + .../TargetDriverDialectManager.java | 70 ++ .../TargetDriverDialectProvider.java | 4 + .../software/amazon/jdbc/util/CacheMap.java | 17 +- .../amazon/jdbc/util/ConnectionUrlParser.java | 16 +- .../amazon/jdbc/util/IamAuthUtils.java | 38 ++ .../amazon/jdbc/util/PropertyUtils.java | 40 +- .../software/amazon/jdbc/util/RdsUtils.java | 18 + .../jdbc/util/SlidingExpirationCache.java | 32 +- ...idingExpirationCacheWithCleanupThread.java | 78 +++ .../amazon/jdbc/util/SqlMethodAnalyzer.java | 6 +- .../util/telemetry/OpenTelemetryFactory.java | 21 +- .../jdbc/wrapper/ConnectionWrapper.java | 99 +-- ..._advanced_jdbc_wrapper_messages.properties | 49 +- wrapper/src/test/build.gradle.kts | 14 +- .../container/ConnectionStringHelper.java | 2 +- .../container/TestDriverProvider.java | 10 + .../aurora/TestPluginServiceImpl.java | 9 +- .../tests/AdvancedPerformanceTest.java | 121 +++- .../DriverConfigurationProfileTests.java | 7 +- .../container/tests/PerformanceTest.java | 190 ++++-- .../ReadWriteSplittingPerformanceTest.java | 2 + .../tests/ReadWriteSplittingTests.java | 31 +- .../integration/host/TestEnvironment.java | 6 +- .../host/TestEnvironmentConfiguration.java | 43 +- .../host/TestEnvironmentProvider.java | 459 +++---------- .../integration/util/ContainerHelper.java | 45 +- .../ConnectionPluginChainBuilderTests.java | 27 +- .../jdbc/ConnectionPluginManagerTests.java | 33 +- .../HikariPooledConnectionProviderTest.java | 48 +- .../amazon/jdbc/PluginServiceImplTests.java | 239 ++++++- .../jdbc/ds/AwsWrapperDataSourceTest.java | 3 +- .../AuroraConnectionTrackerPluginTest.java | 11 +- ...AwsSecretsManagerConnectionPluginTest.java | 32 +- .../plugin/DefaultConnectionPluginTest.java | 7 +- .../ExecutionTimeConnectionPluginTest.java | 6 +- .../plugin/IamAuthConnectionPluginTest.java | 16 +- .../dev/DeveloperConnectionPluginTest.java | 97 ++- .../jdbc/plugin/efm/ConcurrencyTests.java | 191 +++++- ...ClusterAwareWriterFailoverHandlerTest.java | 8 - .../FailoverConnectionPluginTest.java | 31 - .../AdfsCredentialsProviderFactoryTest.java | 113 ++++ .../FederatedAuthPluginTest.java | 188 ++++++ .../ReadWriteSplittingPluginTest.java | 2 - .../states/SessionStateServiceImplTests.java | 417 ++++++++++++ .../jdbc/util/SqlMethodAnalyzerTest.java | 23 + .../resources/federated_auth/adfs-saml.html | 1 + .../federated_auth/adfs-sign-in-page.html | 613 ++++++++++++++++++ .../hibernate_files/hibernate-core.gradle | 2 +- .../hibernate_files/java-module.gradle | 2 +- .../test/resources/simplelogger.properties | 2 - 185 files changed, 9838 insertions(+), 1717 deletions(-) create mode 100644 .github/workflows/mysql_advanced_performance.yml create mode 100644 .github/workflows/pg_advanced_performance.yml create mode 100644 docs/files/configuration-profile-presets.pdf create mode 100644 docs/images/configuration-presets.png create mode 100644 docs/images/session_state_switch_connection.jpg create mode 100644 docs/using-the-jdbc-driver/ConfigurationPresets.md create mode 100644 docs/using-the-jdbc-driver/SessionState.md create mode 100644 docs/using-the-jdbc-driver/using-plugins/UsingTheFederatedAuthPlugin.md create mode 100644 examples/AWSDriverExample/src/main/java/software/amazon/FederatedAuthPluginExample.java create mode 100644 examples/ReadWriteSplittingSample/build.gradle.kts create mode 100644 examples/ReadWriteSplittingSample/gradle.properties create mode 100644 examples/ReadWriteSplittingSample/src/main/java/software/amazon/ReadWriteSplittingSample.java create mode 100644 examples/ReadWriteSplittingSample/src/main/resources/logging.properties create mode 100644 wrapper/src/main/java/software/amazon/jdbc/TargetDriverHelper.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPluginFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/TokenInfo.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPlugin.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPluginFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/Monitor.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorConnectionContext.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorImpl.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorInitializer.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorService.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorServiceImpl.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/CredentialsProviderFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPlugin.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/HttpClientFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/NonValidatingSSLSocketFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/SamlCredentialsProviderFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPlugin.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPluginFactory.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeService.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/NodeResponseTimeMonitor.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfile.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfileBuilder.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfilePresetCodes.java rename wrapper/src/main/java/software/amazon/jdbc/states/{SessionDirtyFlag.java => ResetSessionStateOnCloseCallable.java} (69%) delete mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/RestoreSessionStateCallable.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionState.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionStateField.java delete mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionStateHelper.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionStateService.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionStateServiceImpl.java delete mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/SessionStateTransferCallable.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/states/TransferSessionStateOnSwitchCallable.java rename wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/{MariadbDataSourceHelper.java => MariadbDriverHelper.java} (66%) rename wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/{MysqlConnectorJDataSourceHelper.java => MysqlConnectorJDriverHelper.java} (63%) rename wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/{PgDataSourceHelper.java => PgDriverHelper.java} (64%) create mode 100644 wrapper/src/main/java/software/amazon/jdbc/util/IamAuthUtils.java create mode 100644 wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCacheWithCleanupThread.java create mode 100644 wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactoryTest.java create mode 100644 wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginTest.java create mode 100644 wrapper/src/test/java/software/amazon/jdbc/states/SessionStateServiceImplTests.java create mode 100644 wrapper/src/test/resources/federated_auth/adfs-saml.html create mode 100644 wrapper/src/test/resources/federated_auth/adfs-sign-in-page.html diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 149b0f334..7eb3c6ace 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -21,7 +21,7 @@ jobs: with: fetch-depth: 0 - name: 'Get changed files' - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 id: changed-files-specific with: files_yaml: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c620729de..1798053ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 50 - name: 'Get changed files' id: changed-files-specific - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files_yaml: | doc: diff --git a/.github/workflows/mysql_advanced_performance.yml b/.github/workflows/mysql_advanced_performance.yml new file mode 100644 index 000000000..8297abf14 --- /dev/null +++ b/.github/workflows/mysql_advanced_performance.yml @@ -0,0 +1,55 @@ +name: Run Aurora Mysql Advanced Performance Tests + +on: + workflow_dispatch: + +jobs: + aurora-mysql-performance-tests: + concurrency: AdvancedPerformanceTests-Aurora + name: 'Run Aurora MySQL container advanced performance tests' + runs-on: ubuntu-latest + steps: + - name: 'Clone repository' + uses: actions/checkout@v3 + with: + fetch-depth: 50 + - name: 'Set up JDK 8' + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: 8 + - name: 'Configure AWS credentials' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + - name: 'Set up temp AWS credentials' + run: | + creds=($(aws sts get-session-token \ + --duration-seconds 21600 \ + --query 'Credentials.[AccessKeyId, SecretAccessKey, SessionToken]' \ + --output text \ + | xargs)); + echo "::add-mask::${creds[0]}" + echo "::add-mask::${creds[1]}" + echo "::add-mask::${creds[2]}" + echo "TEMP_AWS_ACCESS_KEY_ID=${creds[0]}" >> $GITHUB_ENV + echo "TEMP_AWS_SECRET_ACCESS_KEY=${creds[1]}" >> $GITHUB_ENV + echo "TEMP_AWS_SESSION_TOKEN=${creds[2]}" >> $GITHUB_ENV + - name: 'Run performance tests (OpenJDK)' + run: | + ./gradlew --no-parallel --no-daemon test-aurora-mysql-advanced-performance + env: + AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }} + AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }} + AWS_SESSION_TOKEN: ${{ env.TEMP_AWS_SESSION_TOKEN }} + - name: 'Archive Performance Results' + if: always() + uses: actions/upload-artifact@v3 + with: + name: 'performance-results' + path: ./wrapper/build/reports/tests/ + retention-days: 5 diff --git a/.github/workflows/mysql_performance.yml b/.github/workflows/mysql_performance.yml index b43d6c770..dfe8ffd67 100644 --- a/.github/workflows/mysql_performance.yml +++ b/.github/workflows/mysql_performance.yml @@ -19,7 +19,7 @@ jobs: distribution: 'corretto' java-version: 8 - name: 'Configure AWS credentials' - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -48,8 +48,8 @@ jobs: AWS_SESSION_TOKEN: ${{ env.TEMP_AWS_SESSION_TOKEN }} - name: 'Archive Performance Results' if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: - name: 'junit-report-performance' + name: 'performance-results' path: ./wrapper/build/reports/tests/ retention-days: 5 diff --git a/.github/workflows/pg_advanced_performance.yml b/.github/workflows/pg_advanced_performance.yml new file mode 100644 index 000000000..61473b2e8 --- /dev/null +++ b/.github/workflows/pg_advanced_performance.yml @@ -0,0 +1,55 @@ +name: Run Aurora Postgres Advanced Performance Tests + +on: + workflow_dispatch: + +jobs: + aurora-postgres-performance-tests: + concurrency: AdvancedPerformanceTests-Aurora + name: 'Run Aurora Postgres container advanced performance tests' + runs-on: ubuntu-latest + steps: + - name: 'Clone repository' + uses: actions/checkout@v3 + with: + fetch-depth: 50 + - name: 'Set up JDK 8' + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: 8 + - name: 'Configure AWS credentials' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + - name: 'Set up temp AWS credentials' + run: | + creds=($(aws sts get-session-token \ + --duration-seconds 21600 \ + --query 'Credentials.[AccessKeyId, SecretAccessKey, SessionToken]' \ + --output text \ + | xargs)); + echo "::add-mask::${creds[0]}" + echo "::add-mask::${creds[1]}" + echo "::add-mask::${creds[2]}" + echo "TEMP_AWS_ACCESS_KEY_ID=${creds[0]}" >> $GITHUB_ENV + echo "TEMP_AWS_SECRET_ACCESS_KEY=${creds[1]}" >> $GITHUB_ENV + echo "TEMP_AWS_SESSION_TOKEN=${creds[2]}" >> $GITHUB_ENV + - name: 'Run performance tests (OpenJDK)' + run: | + ./gradlew --no-parallel --no-daemon test-aurora-pg-advanced-performance + env: + AURORA_CLUSTER_DOMAIN: ${{ secrets.DB_CONN_SUFFIX }} + AURORA_DB_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_ACCESS_KEY_ID: ${{ env.TEMP_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ env.TEMP_AWS_SECRET_ACCESS_KEY }} + AWS_SESSION_TOKEN: ${{ env.TEMP_AWS_SESSION_TOKEN }} + - name: 'Archive Performance Results' + if: always() + uses: actions/upload-artifact@v3 + with: + name: 'performance-results' + path: ./wrapper/build/reports/tests/ + retention-days: 5 diff --git a/.github/workflows/pg_performance.yml b/.github/workflows/pg_performance.yml index 8534b91a8..a94954f0e 100644 --- a/.github/workflows/pg_performance.yml +++ b/.github/workflows/pg_performance.yml @@ -19,7 +19,7 @@ jobs: distribution: 'corretto' java-version: 8 - name: 'Configure AWS credentials' - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -48,8 +48,8 @@ jobs: AWS_SESSION_TOKEN: ${{ env.TEMP_AWS_SESSION_TOKEN }} - name: 'Archive Performance Results' if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: - name: 'junit-report-performance' + name: 'performance-results' path: ./wrapper/build/reports/tests/ retention-days: 5 diff --git a/.github/workflows/run-autoscaling-tests.yml b/.github/workflows/run-autoscaling-tests.yml index 5f4528649..d2baed3d0 100644 --- a/.github/workflows/run-autoscaling-tests.yml +++ b/.github/workflows/run-autoscaling-tests.yml @@ -22,7 +22,7 @@ jobs: distribution: 'corretto' java-version: 8 - name: 'Configure AWS credentials' - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/run-hibernate-orm-tests.yml b/.github/workflows/run-hibernate-orm-tests.yml index 550d794e5..f1e1c1db4 100644 --- a/.github/workflows/run-hibernate-orm-tests.yml +++ b/.github/workflows/run-hibernate-orm-tests.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 50 - name: 'Get changed files' id: changed-files-specific - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files_yaml: | doc: diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 807a1ac47..883a7f7a1 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -25,7 +25,7 @@ jobs: distribution: 'corretto' java-version: 8 - name: 'Configure AWS credentials' - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/run-standard-integration-tests.yml b/.github/workflows/run-standard-integration-tests.yml index aeab6c4a3..c8da513d5 100644 --- a/.github/workflows/run-standard-integration-tests.yml +++ b/.github/workflows/run-standard-integration-tests.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 50 - name: 'Get changed files' id: changed-files-specific - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files_yaml: | doc: diff --git a/CHANGELOG.md b/CHANGELOG.md index d36d19386..1b53d7b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/#semantic-versioning-200). +## [2.3.2] - 2023-12-18 +### :magic_wand: Added +- [Federated Authentication Plugin](https://github.com/awslabs/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/using-plugins/UsingTheFederatedAuthPlugin.md), which supports SAML authentication through ADFS ([PR #741](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/741)). +- [**Experimental** Enhanced Host Monitoring Plugin v2](https://github.com/awslabs/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md#experimental-host-monitoring-plugin-v2), which is a redesign of the original Enhanced Host Monitoring Plugin that addresses memory leaks and high CPU usage during monitoring sessions ([PR #764](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/764)). +- Fastest Response Strategy Plugin, which implements a new autoscaling strategy ([PR #755](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/755)). +- Plugin code for Aurora Initial Connection Strategy Plugin. This plugin returns an instance endpoint when connected using a cluster endpoint ([PR #784](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/784)). + +### :bug: Fixed +- Use existing entries to update the round-robin cache ([PR #739](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/739)). + +### :crab: Changed +- Updated HikariCP example to include configuring the datasource with a JDBC URL ([PR #749](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/749)). +- Replaced the `sychronized` keyword with reentrant locks in AwsCredentialsManager ([PR #785](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/785)). +- Set HostId in HostSpec when connecting using Aurora instance endpoints ([PR #782](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/782)). + +## [2.3.1] - 2023-11-29 +### :magic_wand: Added +- User defined session state transfer functions ([PR #729](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/729)). +- Documentation for using the driver with RDS Multi-AZ database clusters ([PR #740](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/740)). +- [Configuration profiles](https://github.com/awslabs/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#configuration-profiles) and [configuration presets](https://github.com/awslabs/aws-advanced-jdbc-wrapper/blob/main/docs/using-the-jdbc-driver/ConfigurationPresets.md) ([PR #711](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/711) and [PR #738](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/738)). + +### :bug: Fixed +- Stopped monitoring threads causing out of memory errors ([PR #718](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/718)). +- Automatically register a target driver in the class path to prevent `No suitable driver` SQL exceptions ([PR #748](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/748)). + +### :crab: Changed +- Session state tracking to include additional state information ([PR #729](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/729)). +- Log level for intentionally ignored exceptions to reduce the number of warnings ([PR #751](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/751)). + ## [2.3.0] - 2023-11-23 ### :magic_wand: Added - Fast switchover support for Amazon RDS Multi-AZ DB Clusters ([PR #690](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/690)). @@ -172,7 +201,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### :crab: Changed - Lock initialization of `AuroraHostListProvider` ([PR #347](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/347)). -- Optimized thread locks and expiring cache for the Enhanced Monitoring Plugin. ([PR #365](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/365)). +- Optimized thread locks and expiring cache for the Enhanced Monitoring Plugin ([PR #365](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/365)). - Updated Hibernate sample code to reflect changes in the wrapper source code ([PR #368](https://github.com/awslabs/aws-advanced-jdbc-wrapper/pull/368)). - Updated KnownLimitations.md to reflect that Amazon RDS Blue/Green Deployments are not supported. See [Amazon RDS Blue/Green Deployments](./docs/KnownLimitations.md#amazon-rds-blue-green-deployments). @@ -210,7 +239,9 @@ The Amazon Web Services (AWS) Advanced JDBC Driver allows an application to take - The [AWS IAM Authentication Connection Plugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) - The [AWS Secrets Manager Connection Plugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheAwsSecretsManagerPlugin.md) -[2.3.0]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.2.4...2.3.0 +[2.3.2]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.3.1...2.3.2 +[2.3.1]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.3.0...2.3.1 +[2.3.0]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.2.5...2.3.0 [2.2.5]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.2.4...2.2.5 [2.2.4]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.2.3...2.2.4 [2.2.3]: https://github.com/awslabs/aws-advanced-jdbc-wrapper/compare/2.2.2...2.2.3 diff --git a/Maintenance.md b/Maintenance.md index 079e00b60..0996da0fe 100644 --- a/Maintenance.md +++ b/Maintenance.md @@ -1,30 +1,31 @@ -# Release Schedule and Maintenance Policy +# Release Schedule -| Release Date | Release | -|------------------|--------------------------------------------------------------------------------------------| -| October 5, 2022 | [Release 1.0.0](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/1.0.0) | -| January 31, 2023 | [Release 1.0.1](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/1.0.1) | -| Mar 30, 2023 | [Release 1.0.2](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/1.0.2) | -| April 28, 2023 | [Release 2.0.0](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.0.0) | -| May 11, 2023 | [Release 2.1.0](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.1.0) | -| May 21, 2023 | [Release 2.1.1](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.1.1) | -| June 14, 2023 | [Release 2.2.0](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.0) | -| June 16, 2023 | [Release 2.2.1](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.1) | -| July 5, 2023 | [Release 2.2.2](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.2) | -| July 31, 2023 | [Release 2.2.3](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.3) | -| August 25, 2023 | [Release 2.2.4](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.4) | -| October 3, 2023 | [Release 2.2.5](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.2.5) | -| November, 2023 | [Release 2.3.0](https://github.com/awslabs/`aws-advanced-jdbc-wrapper`/releases/tag/2.3.0) | +| Release Date | Release | +|-------------------|------------------------------------------------------------------------------------------| +| October 5, 2022 | [Release 1.0.0](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/1.0.0) | +| January 31, 2023 | [Release 1.0.1](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/1.0.1) | +| Mar 30, 2023 | [Release 1.0.2](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/1.0.2) | +| April 28, 2023 | [Release 2.0.0](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.0.0) | +| May 11, 2023 | [Release 2.1.0](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.1.0) | +| May 21, 2023 | [Release 2.1.1](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.1.1) | +| June 14, 2023 | [Release 2.2.0](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.0) | +| June 16, 2023 | [Release 2.2.1](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.1) | +| July 5, 2023 | [Release 2.2.2](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.2) | +| July 31, 2023 | [Release 2.2.3](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.3) | +| August 25, 2023 | [Release 2.2.4](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.4) | +| October 3, 2023 | [Release 2.2.5](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.2.5) | +| November 15, 2023 | [Release 2.3.0](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.3.0) | +| November 29, 2023 | [Release 2.3.1](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.3.1) | +| December 18, 2023 | [Release 2.3.2](https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/tag/2.3.2) | -``aws-advanced-jdbc-wrapper`` [follows semver](https://semver.org/#semantic-versioning-200) which means we will only +`aws-advanced-jdbc-wrapper` [follows semver](https://semver.org/#semantic-versioning-200) which means we will only release breaking changes in major versions. Generally speaking patches will be released to fix existing problems without adding new features. Minor version releases will include new features as well as fixes to existing features. We will do -our -best to deprecate existing features before removing them completely. +our best to deprecate existing features before removing them completely. For minor version releases, `aws-advanced-jdbc-wrapper` uses a “release-train” model. Approximately every four weeks we release a new minor version which includes all the new features and fixes that are ready to go. -Having a set release schedule makes sure ``aws-advanced-jdbc-wrapper`` is released in a predictable way and prevents a +Having a set release schedule makes sure `aws-advanced-jdbc-wrapper` is released in a predictable way and prevents a backlog of unreleased changes. In contrast, `aws-advanced-jdbc-wrapper` releases new major versions only when there are a critical mass of @@ -36,7 +37,7 @@ Please note: Both the roadmap and the release dates reflect intentions rather th as we learn more or encounter unexpected issues. If dates do need to change, we will be as transparent as possible, and log all changes in the changelog at the bottom of this page. -Maintenance Policy +# Maintenance Policy For `aws-advanced-jdbc-wrapper` new features and active development always takes place against the newest version. The `aws-advanced-jdbc-wrapper` project follows the semantic versioning specification for assigning version numbers @@ -69,4 +70,4 @@ from the updated source after the PRs are merged. | Major Version | Latest Minor Version | Status | Initial Release | Maintenance Window Start | Maintenance Window End | |---------------|----------------------|-------------|-----------------|--------------------------|------------------------| | 1 | 1.0.2 | Maintenance | Oct 5, 2022 | Apr 28, 2023 | Apr 28, 2024 | -| 2 | 2.3.0 | Current | Apr 28, 2023 | N/A | N/A | +| 2 | 2.3.2 | Current | Apr 28, 2023 | N/A | N/A | diff --git a/README.md b/README.md index 0b3294d3f..08ed696fb 100644 --- a/README.md +++ b/README.md @@ -66,32 +66,37 @@ You can find our driver by searching in The Central Repository with GroupId and ## Properties -| Parameter | Reference | Documentation Link | -|----------------------------------------|:--------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------:| -| `wrapperDialect` | `DialectManager.DIALECT` | [Dialects](/docs/using-the-jdbc-driver/DatabaseDialects.md), and whether you should include it. | -| `wrapperPlugins` | `PropertyDefinition.PLUGINS` | | -| `secretsManagerSecretId` | `AwsSecretsManagerConnectionPlugin.SECRET_ID_PROPERTY` | [SecretsManagerPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheAwsSecretsManagerPlugin.md) | -| `secretsManagerRegion` | `AwsSecretsManagerConnectionPlugin.REGION_PROPERTY` | [SecretsManagerPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheAwsSecretsManagerPlugin.md) | -| `wrapperDriverName` | `DriverMetaDataConnectionPlugin.WRAPPER_DRIVER_NAME` | [DriverMetaDataConnectionPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheDriverMetadataConnectionPlugin.md) | -| `failoverMode` | `FailoverConnectionPlugin.FAILOVER_MODE` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `clusterInstanceHostPattern` | `AuroraHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `enableClusterAwareFailover` | `FailoverConnectionPlugin.ENABLE_CLUSTER_AWARE_FAILOVER` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `failoverClusterTopologyRefreshRateMs` | `FailoverConnectionPlugin.FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `failoverReaderConnectTimeoutMs` | `FailoverConnectionPlugin.FAILOVER_READER_CONNECT_TIMEOUT_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `failoverTimeoutMs` | `FailoverConnectionPlugin.FAILOVER_TIMEOUT_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `failoverWriterReconnectIntervalMs` | `FailoverConnectionPlugin.FAILOVER_WRITER_RECONNECT_INTERVAL_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | -| `failureDetectionCount` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | -| `failureDetectionEnabled` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_ENABLED` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | -| `failureDetectionInterval` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | -| `failureDetectionTime` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | -| `monitorDisposalTime` | `MonitorServiceImpl.MONITOR_DISPOSAL_TIME_MS` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | -| `iamDefaultPort` | `IamAuthConnectionPlugin.IAM_DEFAULT_PORT` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | -| `iamHost` | `IamAuthConnectionPlugin.IAM_HOST` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | -| `iamRegion` | `IamAuthConnectionPlugin.IAM_REGION` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | -| `iamExpiration` | `IamAuthConnectionPlugin.IAM_EXPIRATION` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | -| `wrapperLogUnclosedConnections` | `PropertyDefinition.LOG_UNCLOSED_CONNECTIONS` | [LogUnclosedConnections](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#logging) | -| `wrapperLoggerLevel` | `PropertyDefinition.LOGGER_LEVEL` | [LoggingLevel](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#logging) | -| `wrapperProfileName` | `PropertyDefinition.PROFILE_NAME` | [ConfigurationProfiles](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#configuration-profiles) | +| Parameter | Reference | Documentation Link | +|----------------------------------------|:--------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------:| +| `wrapperDialect` | `DialectManager.DIALECT` | [Dialects](/docs/using-the-jdbc-driver/DatabaseDialects.md), and whether you should include it. | +| `wrapperPlugins` | `PropertyDefinition.PLUGINS` | | +| `secretsManagerSecretId` | `AwsSecretsManagerConnectionPlugin.SECRET_ID_PROPERTY` | [SecretsManagerPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheAwsSecretsManagerPlugin.md) | +| `secretsManagerRegion` | `AwsSecretsManagerConnectionPlugin.REGION_PROPERTY` | [SecretsManagerPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheAwsSecretsManagerPlugin.md) | +| `wrapperDriverName` | `DriverMetaDataConnectionPlugin.WRAPPER_DRIVER_NAME` | [DriverMetaDataConnectionPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheDriverMetadataConnectionPlugin.md) | +| `failoverMode` | `FailoverConnectionPlugin.FAILOVER_MODE` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `clusterInstanceHostPattern` | `AuroraHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `enableClusterAwareFailover` | `FailoverConnectionPlugin.ENABLE_CLUSTER_AWARE_FAILOVER` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `failoverClusterTopologyRefreshRateMs` | `FailoverConnectionPlugin.FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `failoverReaderConnectTimeoutMs` | `FailoverConnectionPlugin.FAILOVER_READER_CONNECT_TIMEOUT_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `failoverTimeoutMs` | `FailoverConnectionPlugin.FAILOVER_TIMEOUT_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `failoverWriterReconnectIntervalMs` | `FailoverConnectionPlugin.FAILOVER_WRITER_RECONNECT_INTERVAL_MS` | [FailoverPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md) | +| `failureDetectionCount` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | +| `failureDetectionEnabled` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_ENABLED` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | +| `failureDetectionInterval` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | +| `failureDetectionTime` | `HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | +| `monitorDisposalTime` | `MonitorServiceImpl.MONITOR_DISPOSAL_TIME_MS` | [HostMonitoringPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md) | +| `iamDefaultPort` | `IamAuthConnectionPlugin.IAM_DEFAULT_PORT` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | +| `iamHost` | `IamAuthConnectionPlugin.IAM_HOST` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | +| `iamRegion` | `IamAuthConnectionPlugin.IAM_REGION` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | +| `iamExpiration` | `IamAuthConnectionPlugin.IAM_EXPIRATION` | [IamAuthenticationPlugin](./docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md) | +| `wrapperLogUnclosedConnections` | `PropertyDefinition.LOG_UNCLOSED_CONNECTIONS` | [AWS Advanced JDBC Driver Parameters](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#aws-advanced-jdbc-driver-parameters) | +| `wrapperLoggerLevel` | `PropertyDefinition.LOGGER_LEVEL` | [Logging](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#logging) | +| `wrapperProfileName` | `PropertyDefinition.PROFILE_NAME` | [Configuration Profiles](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#configuration-profiles) | +| `autoSortWrapperPluginOrder` | `PropertyDefinition.AUTO_SORT_PLUGIN_ORDER` | [Plugins](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#plugins) | +| `loginTimeout` | `PropertyDefinition.LOGIN_TIMEOUT` | [AWS Advanced JDBC Driver Parameters](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#aws-advanced-jdbc-driver-parameters) | +| `connectTimeout` | `PropertyDefinition.CONNECT_TIMEOUT` | [AWS Advanced JDBC Driver Parameters](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#aws-advanced-jdbc-driver-parameters) | +| `socketTimeout` | `PropertyDefinition.SOCKET_TIMEOUT` | [AWS Advanced JDBC Driver Parameters](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#aws-advanced-jdbc-driver-parameters) | +| `tcpKeepAlive` | `PropertyDefinition.TCP_KEEP_ALIVE` | [AWS Advanced JDBC Driver Parameters](./docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#aws-advanced-jdbc-driver-parameters) | **A Secret ARN** has the following format: `arn:aws:secretsmanager:::secret:SecretName-6RandomCharacters` diff --git a/benchmarks/README.md b/benchmarks/README.md index 8c4463876..50c80d664 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -7,5 +7,5 @@ The benchmarks do not measure the performance of target JDBC drivers nor the per ## Usage 1. Build the benchmarks with the following command `../gradlew jmhJar`. 1. the JAR file will be outputted to `build/libs` -2. Run the benchmarks with the following command `java -jar build/libs/benchmarks-2.3.0-jmh.jar`. +2. Run the benchmarks with the following command `java -jar build/libs/benchmarks-2.3.2-jmh.jar`. 1. you may have to update the command based on the exact version of the produced JAR file diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index e7dd5bb34..ea97e4a26 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -20,9 +20,9 @@ plugins { dependencies { jmhImplementation(project(":aws-advanced-jdbc-wrapper")) - implementation("org.postgresql:postgresql:42.6.0") + implementation("org.postgresql:postgresql:42.7.1") implementation("mysql:mysql-connector-java:8.0.33") - implementation("org.mariadb.jdbc:mariadb-java-client:3.3.0") + implementation("org.mariadb.jdbc:mariadb-java-client:3.3.1") implementation("com.zaxxer:HikariCP:4.0.3") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") 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 7de7d1f31..bb8260771 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java @@ -71,6 +71,9 @@ import software.amazon.jdbc.util.telemetry.TelemetryCounter; import software.amazon.jdbc.util.telemetry.TelemetryFactory; import software.amazon.jdbc.util.telemetry.TelemetryGauge; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.ConfigurationProfileBuilder; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.wrapper.ConnectionWrapper; @State(Scope.Benchmark) @@ -101,6 +104,7 @@ public class ConnectionPluginManagerBenchmarks { @Mock TelemetryContext mockTelemetryContext; @Mock TelemetryCounter mockTelemetryCounter; @Mock TelemetryGauge mockTelemetryGauge; + ConfigurationProfile configurationProfile; private AutoCloseable closeable; public static void main(String[] args) throws RunnerException { @@ -120,7 +124,11 @@ public void setUpIteration() throws Exception { when(mockConnectionProvider.connect(anyString(), any(Properties.class))).thenReturn( mockConnection); - when(mockConnectionProvider.connect(anyString(), any(Dialect.class), any(HostSpec.class), + when(mockConnectionProvider.connect( + anyString(), + any(Dialect.class), + any(TargetDriverDialect.class), + any(HostSpec.class), any(Properties.class))).thenReturn(mockConnection); when(mockTelemetryFactory.openTelemetryContext(anyString(), any())).thenReturn(mockTelemetryContext); when(mockTelemetryFactory.openTelemetryContext(eq(null), any())).thenReturn(mockTelemetryContext); @@ -140,9 +148,11 @@ public void setUpIteration() throws Exception { final List> pluginFactories = new ArrayList<>( Collections.nCopies(10, BenchmarkPluginFactory.class)); - DriverConfigurationProfiles.addOrReplaceProfile( - "benchmark", - pluginFactories); + configurationProfile = ConfigurationProfileBuilder.get() + .withName("benchmark") + .withPluginFactories(pluginFactories) + .build(); + propertiesWithoutPlugins = new Properties(); propertiesWithoutPlugins.setProperty(PropertyDefinition.PLUGINS.name, ""); @@ -152,12 +162,15 @@ public void setUpIteration() throws Exception { TelemetryFactory telemetryFactory = new DefaultTelemetryFactory(propertiesWithPlugins); - pluginManager = new ConnectionPluginManager(mockConnectionProvider, mockConnectionWrapper, telemetryFactory); - pluginManager.init(mockPluginService, propertiesWithPlugins, mockPluginManagerService); + pluginManager = new ConnectionPluginManager(mockConnectionProvider, + null, + mockConnectionWrapper, + telemetryFactory); + pluginManager.init(mockPluginService, propertiesWithPlugins, mockPluginManagerService, configurationProfile); - pluginManagerWithNoPlugins = new ConnectionPluginManager(mockConnectionProvider, + pluginManagerWithNoPlugins = new ConnectionPluginManager(mockConnectionProvider, null, mockConnectionWrapper, telemetryFactory); - pluginManagerWithNoPlugins.init(mockPluginService, propertiesWithoutPlugins, mockPluginManagerService); + pluginManagerWithNoPlugins.init(mockPluginService, propertiesWithoutPlugins, mockPluginManagerService, null); } @TearDown(Level.Iteration) @@ -167,17 +180,17 @@ public void tearDownIteration() throws Exception { @Benchmark public ConnectionPluginManager initConnectionPluginManagerWithNoPlugins() throws SQLException { - final ConnectionPluginManager manager = new ConnectionPluginManager(mockConnectionProvider, + final ConnectionPluginManager manager = new ConnectionPluginManager(mockConnectionProvider, null, mockConnectionWrapper, mockTelemetryFactory); - manager.init(mockPluginService, propertiesWithoutPlugins, mockPluginManagerService); + manager.init(mockPluginService, propertiesWithoutPlugins, mockPluginManagerService, configurationProfile); return manager; } @Benchmark public ConnectionPluginManager initConnectionPluginManagerWithPlugins() throws SQLException { - final ConnectionPluginManager manager = new ConnectionPluginManager(mockConnectionProvider, + final ConnectionPluginManager manager = new ConnectionPluginManager(mockConnectionProvider, null, mockConnectionWrapper, mockTelemetryFactory); - manager.init(mockPluginService, propertiesWithPlugins, mockPluginManagerService); + manager.init(mockPluginService, propertiesWithPlugins, mockPluginManagerService, configurationProfile); return manager; } 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 e96f10e8e..6cabcf6fa 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java @@ -65,6 +65,7 @@ import software.amazon.jdbc.util.telemetry.TelemetryCounter; import software.amazon.jdbc.util.telemetry.TelemetryFactory; import software.amazon.jdbc.util.telemetry.TelemetryGauge; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.wrapper.ConnectionWrapper; @State(Scope.Benchmark) @@ -87,6 +88,7 @@ public class PluginBenchmarks { .host(TEST_HOST).port(TEST_PORT).build(); @Mock private PluginService mockPluginService; + @Mock private Dialect mockDialect; @Mock private ConnectionPluginManager mockConnectionPluginManager; @Mock private TelemetryFactory mockTelemetryFactory; @Mock TelemetryContext mockTelemetryContext; @@ -127,7 +129,11 @@ public void setUpIteration() throws Exception { when(mockTelemetryFactory.createGauge(anyString(), any(GaugeCallable.class))).thenReturn(mockTelemetryGauge); when(mockConnectionProvider.connect(anyString(), any(Properties.class))).thenReturn( mockConnection); - when(mockConnectionProvider.connect(anyString(), any(Dialect.class), any(HostSpec.class), + when(mockConnectionProvider.connect( + anyString(), + any(Dialect.class), + any(TargetDriverDialect.class), + any(HostSpec.class), any(Properties.class))).thenReturn(mockConnection); when(mockConnection.createStatement()).thenReturn(mockStatement); when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); @@ -139,6 +145,7 @@ public void setUpIteration() throws Exception { when(mockStatement.getConnection()).thenReturn(mockConnection); when(this.mockPluginService.acceptsStrategy(any(), eq("random"))).thenReturn(true); when(this.mockPluginService.getCurrentHostSpec()).thenReturn(writerHostSpec); + when(this.mockPluginService.getDialect()).thenReturn(mockDialect); } @TearDown(Level.Iteration) @@ -302,11 +309,14 @@ public ResultSet executeStatementWithExecutionTimePlugin() throws SQLException { @Benchmark public ResultSet executeStatementWithTelemetryDisabled() throws SQLException { try ( - ConnectionWrapper wrapper = new ConnectionWrapper( + ConnectionWrapper wrapper = new TestConnectionWrapper( disabledTelemetry(), CONNECTION_STRING, - mockConnectionProvider, - mockTelemetryFactory); + mockConnectionPluginManager, + mockTelemetryFactory, + mockPluginService, + mockHostListProviderService, + mockPluginManagerService); Statement statement = wrapper.createStatement(); ResultSet resultSet = statement.executeQuery("some sql")) { return resultSet; @@ -316,11 +326,14 @@ public ResultSet executeStatementWithTelemetryDisabled() throws SQLException { @Benchmark public ResultSet executeStatementWithTelemetry() throws SQLException { try ( - ConnectionWrapper wrapper = new ConnectionWrapper( + ConnectionWrapper wrapper = new TestConnectionWrapper( useTelemetry(), CONNECTION_STRING, - mockConnectionProvider, - mockTelemetryFactory); + mockConnectionPluginManager, + mockTelemetryFactory, + mockPluginService, + mockHostListProviderService, + mockPluginManagerService); Statement statement = wrapper.createStatement(); ResultSet resultSet = statement.executeQuery("some sql")) { return resultSet; diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index da625c525..4e9061a4d 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -23,9 +23,9 @@ - - - + + + diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index d750605fe..d5050ecb8 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -16,7 +16,7 @@ If you are using the AWS JDBC Driver as part of a Gradle project, include the wr ```gradle dependencies { - implementation group: 'software.amazon.jdbc', name: 'aws-advanced-jdbc-wrapper', version: '2.3.0' + implementation group: 'software.amazon.jdbc', name: 'aws-advanced-jdbc-wrapper', version: '2.3.2' implementation group: 'org.postgresql', name: 'postgresql', version: '42.5.0' } ``` @@ -30,13 +30,13 @@ You can use pre-compiled packages that can be downloaded directly from [GitHub R For example, the following command uses wget to download the wrapper: ```bash -wget https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/download/2.3.0/aws-advanced-jdbc-wrapper-2.3.0.jar +wget https://github.com/awslabs/aws-advanced-jdbc-wrapper/releases/download/2.3.2/aws-advanced-jdbc-wrapper-2.3.2.jar ``` Then, the following command adds the AWS JDBC Driver to the CLASSPATH: ```bash -export CLASSPATH=$CLASSPATH:/home/userx/libs/aws-advanced-jdbc-wrapper-2.3.0.jar +export CLASSPATH=$CLASSPATH:/home/userx/libs/aws-advanced-jdbc-wrapper-2.3.2.jar ``` ### As a Maven Dependency @@ -48,7 +48,7 @@ You can use [Maven's dependency management](https://search.maven.org/search?q=g: software.amazon.jdbc aws-advanced-jdbc-wrapper - 2.3.0 + 2.3.2 ``` @@ -59,7 +59,7 @@ You can use [Gradle's dependency management](https://search.maven.org/search?q=g ```gradle dependencies { - implementation group: 'software.amazon.jdbc', name: 'aws-advanced-jdbc-wrapper', version: '2.3.0' + implementation group: 'software.amazon.jdbc', name: 'aws-advanced-jdbc-wrapper', version: '2.3.2' } ``` @@ -67,7 +67,7 @@ To add a Gradle dependency in a Kotlin syntax, use the following configuration: ```kotlin dependencies { - implementation("software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.3.0") + implementation("software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.3.2") } ``` diff --git a/docs/files/configuration-profile-presets.pdf b/docs/files/configuration-profile-presets.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2f06cff5963bd51bf300c0406f67444dce38bfaa GIT binary patch literal 141176 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%0Axn_=}s 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 a0R#|0009ILKmY**5I_I{1Q7VY1bzbem#At0 literal 0 HcmV?d00001 diff --git a/docs/images/configuration-presets.png b/docs/images/configuration-presets.png new file mode 100644 index 0000000000000000000000000000000000000000..38ad73165b246677f525f3d539ad627acee666a9 GIT binary patch literal 135055 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

Z+P~zqEnp_fKwR+Z`8j|-06?t`5h9msu0Ov4 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 v?;Stg|Awi#aF#>w>!U*I7tp(*Bv&VA literal 0 HcmV?d00001 diff --git a/docs/images/session_state_switch_connection.jpg b/docs/images/session_state_switch_connection.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42b22550fe811e39c7abb38a33ef755788dcb623 GIT binary patch literal 59399 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 literal 0 HcmV?d00001 diff --git a/docs/using-the-jdbc-driver/ConfigurationPresets.md b/docs/using-the-jdbc-driver/ConfigurationPresets.md new file mode 100644 index 000000000..aefb04b9c --- /dev/null +++ b/docs/using-the-jdbc-driver/ConfigurationPresets.md @@ -0,0 +1,42 @@ +# Configuration Presets + +## What is a Configuration Preset? + +A Configuration Preset is a [configuration profile](./UsingTheJdbcDriver.md#configuration-profiles) that has already been set up by the AWS JDBC Driver team. Preset configuration profiles are optimized, profiled, verified and can be used right away. If the existing presets do not cover an exact use case, users can also create their own configuration profiles based on the built-in presets. + +## Using Configuration Presets + +The Configuration Preset name should be specified with the [`wrapperProfileName`](#connection-plugin-manager-parameters) parameter. + +```java +properties.setProperty("wrapperProfileName", "A2"); +``` + +Users can create their own custom configuration profiles based on built-in configuration presets. + +Users can not delete built-in configuration presets. + +```java +// Create a new configuration profile "myNewProfile" based on "A2" configuration preset +ConfigurationProfileBuilder.from("A2") + .withName("myNewProfile") + .withDialect(new CustomDatabaseDialect()) +.buildAndSet(); + +properties.setProperty("wrapperProfileName", "myNewProfile"); +``` + +## Existing Configuration Presets + +Configuration Presets are optimized for 3 main user scenarios. They are: +- **No connection pool** preset family: `A`, `B`, `C` +- AWS JDBC Driver **Internal connection pool** preset family: `D`, `E`, `F` +- **External connection pool** preset family: `G`, `H`, `I` + +Some preset names may include a number, like `A0`, `A1`, `A2`, `D0`, `D1`, etc. Usually, the number represent sensitivity or timing variations for the same preset. For example, `A0` is optimized for normal network outage sensitivity and normal response time, while `A1` is less sensitive. Please take into account that more aggressive presets tend to cause more false positive failure detections. More details can be found in this file: [ConfigurationProfilePresetCodes.java](./../../wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfilePresetCodes.java) + +Choosing the right configuration preset for your application can be a challenging task. Many presets could potentially fit the needs of your application. Various user application requirements and goals are presented in the following table and organized to help you identify the most suitable presets for your application. + +PDF version of the following table can be found [here](./../files/configuration-profile-presets.pdf). + +

diff --git a/docs/using-the-jdbc-driver/DataSource.md b/docs/using-the-jdbc-driver/DataSource.md index 9367c1f0c..470225d14 100644 --- a/docs/using-the-jdbc-driver/DataSource.md +++ b/docs/using-the-jdbc-driver/DataSource.md @@ -64,6 +64,9 @@ To use the AWS JDBC Driver with a connection pool, you must: ds.addDataSourceProperty("serverName", "db-identifier.cluster-XYZ.us-east-2.rds.amazonaws.com"); ds.addDataSourceProperty("serverPort", "5432"); ds.addDataSourceProperty("database", "postgres"); + + // Alternatively, the AwsWrapperDataSource can be configured with a JDBC URL instead of individual properties as seen above. + ds.addDataSourceProperty("jdbcUrl", "jdbc:aws-wrapper:postgresql://db-identifier.cluster-XYZ.us-east-2.rds.amazonaws.com:5432/postgres"); ``` 4. Set the driver-specific datasource: @@ -79,7 +82,7 @@ To use the AWS JDBC Driver with a connection pool, you must: ds.addDataSourceProperty("targetDataSourceProperties", targetDataSourceProps); ``` -> **:warning:Note:** HikariCP supports either DataSource-based configuration or DriverManager-based configuration by specifying the `dataSourceClassName` or the `jdbcUrl`. When using the `AwsWrapperDataSource` you must specify the `dataSourceClassName`, therefore `HikariDataSource.setJdbcUrl` is not supported. For more information see HikariCP's [documentation](https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby). +> **:warning:Note:** HikariCP supports either DataSource-based configuration or DriverManager-based configuration by specifying the `dataSourceClassName` or the `jdbcUrl`. When using the `AwsWrapperDataSource` you must specify the `dataSourceClassName`, and the `HikariDataSource.setJdbcUrl` method should not be used. For more information see HikariCP's [documentation](https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby). ### Examples See [here](../../examples/AWSDriverExample/src/main/java/software/amazon/DatasourceExample.java) for a simple AWS Driver Datasource example. diff --git a/docs/using-the-jdbc-driver/SessionState.md b/docs/using-the-jdbc-driver/SessionState.md new file mode 100644 index 000000000..75eacf847 --- /dev/null +++ b/docs/using-the-jdbc-driver/SessionState.md @@ -0,0 +1,42 @@ +# Session States + +## What is a session state? + +Every connection is associated with a connection session on the server and a group of related session settings like the autoCommit flag or the transaction isolation level. The following session settings are tracked by the AWS JDBC Driver and together they form a session state: +- autoCommit (`setAutoCommit`, `getAutoCommit`) +- readOnly (`isReadOnly`, `setReadOnly`) +- transaction isolation level (`setTransactionIsolation`, `getTransactionIsolation`) +- holdability (`setHoldability`, `getHoldability`) +- network timeout (`setNetworkTimeout`, `getNetworkTimeout`) +- catalog (`setCatalog`, `getCatalog`) +- schema (`setSchema`, `getSchema`) +- types mapping (`setTypeMap`, `getTypeMap`) + +Since the AWS JDBC Driver can transparently switch physical connection to a server (for instance, during a cluster failover), it's important to re-apply a current session state to a new connection during such switch. + +## Tracking Session States Changes +
diagram for the session state transfer
+ +The diagram above shows the process of switching one database connection `A` to a new connection `B`. After connection `A` is established, it's returned to the user application. A user application may use this connection to query data from the database as well as to change some session settings. For example, if the user application calls `setReadOnly` on a connection, the AWS JDBC Driver intercepts this call and stores a new session setting for the `readOnly` setting. At the same time, the driver verifies if the original session setting is known or not. If the original setting is not known, the driver will make an additional `getReadOnly` call and store the result as a pristine value in order to save the original session setting. Later, the driver may need the pristine value to restore the connection session state to its original state. + +## Restore to the Original Session State + +Before closing an existing connection, the AWS JDBC Driver may try to reset all changes to the session state made by the user application. Some application frameworks and connection pools, like the Spring Framework or HikariCP, intercept calls to `close()` and may perform additional connection configuration. Since the AWS JDBC Driver might change the internal physical connection to a server, a new physical connection's settings may become unexpected to the user application and may cause errors. It is also important to mention that calling `close()` on a connection while using connection pooling doesn't close the connection or stop communicating to a server. Instead the connection is returned to a pool of available connections. Cleaning up a session state before returning a connection to a pool is necessary to avoid side effects and errors when a connection is retrieved from a pool to be reused. + +Before closing a connection, the AWS JDBC Driver sets its session state settings with the pristine values that have been previously stored in the driver. If a pristine value isn't available, it means that there have been no changes to that particular setting made by the user application, and that it's safe to assume that this setting is in its original/unchanged state. + +Session state reset could be disabled by using `resetSessionStateOnClose` configuration parameter. + +## Transfer Session State to a new Connection + +When the driver needs to switch to a new connection, it opens a new connection and transfers a session state to it. All current session state values are applied to the new connection. Pristine values for a new connection are also fetched and stored if needed. When a new connection is configured, it replaces the current internal connection. + +Session transfer cab be disabled by using the `transferSessionStateOnSwitch` configuration parameter. + +## Session State Custom handlers + +It's possible to extend or replace existing logic of resetting session state and transferring session state with custom handlers. Use the following methods on `software.amazon.jdbc.Driver` class to set and reset custom handlers: +- `setResetSessionStateOnCloseFunc` +- `resetResetSessionStateOnCloseFunc` +- `setTransferSessionStateOnSwitchFunc` +- `resetTransferSessionStateOnSwitchFunc` diff --git a/docs/using-the-jdbc-driver/UsingTheJdbcDriver.md b/docs/using-the-jdbc-driver/UsingTheJdbcDriver.md index 576fd3c5a..dec7e1d91 100644 --- a/docs/using-the-jdbc-driver/UsingTheJdbcDriver.md +++ b/docs/using-the-jdbc-driver/UsingTheJdbcDriver.md @@ -47,14 +47,22 @@ The AWS JDBC Driver also has a parameter, [`wrapperLoggerLevel`](#aws-advanced-j ## AWS Advanced JDBC Driver Parameters These parameters are applicable to any instance of the AWS JDBC Driver. -| Parameter | Value | Required | Description | Default Value | -|---------------------------------|-----------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| -| `wrapperLogUnclosedConnections` | `Boolean` | No | Allows the AWS JDBC Driver to track a point in the code where connection has been opened but not closed. | `false` | -| `wrapperLoggerLevel` | `String` | No | Logger level of the AWS JDBC Driver.

If it is used, it must be one of the following values: `OFF`, `SEVERE`, `WARNING`, `INFO`, `CONFIG`, `FINE`, `FINER`, `FINEST`, `ALL`. | `null` | -| `database` | `String` | No | Database name. | `null` | -| `user` | `String` | No | Database username. | `null` | -| `password` | `String` | No | Database password. | `null` | -| `wrapperDialect` | `String` | No | Please see [this page on database dialects](/docs/using-the-jdbc-driver/DatabaseDialects.md), and whether you should include it. | `null` | +| Parameter | Value | Required | Description | Default Value | +|---------------------------------|-----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| `wrapperLoggerLevel` | `String` | No | Logger level of the AWS JDBC Driver.

If it is used, it must be one of the following values: `OFF`, `SEVERE`, `WARNING`, `INFO`, `CONFIG`, `FINE`, `FINER`, `FINEST`, `ALL`. | `null` | +| `database` | `String` | No | Database name. | `null` | +| `user` | `String` | No | Database username. | `null` | +| `password` | `String` | No | Database password. | `null` | +| `wrapperDialect` | `String` | No | Please see [this page on database dialects](/docs/using-the-jdbc-driver/DatabaseDialects.md), and whether you should include it. | `null` | +| `wrapperLogUnclosedConnections` | `Boolean` | No | Allows the AWS JDBC Driver to capture a stacktrace for each connection that is opened. If the `finalize()` method is reached without the connection being closed, the stacktrace is printed to the log. This helps developers to detect and correct the source of potential connection leaks. | `false` | +| `loginTimeout` | `Integer` | No | Login timeout in milliseconds. | `null` | +| `connectTimeout` | `Integer` | No | Socket connect timeout in milliseconds. | `null` | +| `socketTimeout` | `Integer` | No | Socket timeout in milliseconds. | `null` | +| `tcpKeepAlive` | `Boolean` | No | Enable or disable TCP keep-alive probe. | `false` | +| `targetDriverAutoRegister` | `Boolean` | No | Allows the AWS JDBC Driver to register a target driver based on `wrapperTargetDriverDialect` configuration parameter or, if it's missed, on a connection url protocol. | `true` | +| `transferSessionStateOnSwitch` | `Boolean` | No | Enables transferring the session state to a new connection. | `true` | +| `resetSessionStateOnClose` | `Boolean` | No | Enables resetting the session state before closing connection. | `true` | +| `rollbackOnSwitch` | `Boolean` | No | Enables rolling back a current transaction, if any in effect, before switching to a new connection. | `true` | ## Plugins The AWS JDBC Driver uses plugins to execute JDBC methods. You can think of a plugin as an extensible code module that adds extra logic around any JDBC method calls. The AWS JDBC Driver has a number of [built-in plugins](#list-of-available-plugins) available for use. @@ -63,10 +71,11 @@ Plugins are loaded and managed through the Connection Plugin Manager and may be ### Connection Plugin Manager Parameters -| Parameter | Value | Required | Description | Default Value | -|----------------------|----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| -| `wrapperPlugins` | `String` | No | Comma separated list of connection plugin codes.

Example: `failover,efm` | `auroraConnectionTracker,failover,efm` | -| `wrapperProfileName` | `String` | No | Driver configuration profile name. Instead of listing plugin codes with `wrapperPlugins`, the driver profile can be set with this parameter.

Example: See [below](#configuration-profiles). | `null` | +| Parameter | Value | Required | Description | Default Value | +|-----------------------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| `wrapperPlugins` | `String` | No | Comma separated list of connection plugin codes.

Example: `failover,efm` | `auroraConnectionTracker,failover,efm` | +| `autoSortWrapperPluginOrder` | `Boolean` | No | Allows the AWS JDBC Driver to sort connection plugins to prevent plugin misconfiguration. Allows a user to provide a custom plugin order if needed. | `true` | +| `wrapperProfileName` | `String` | No | Driver configuration profile name. Instead of listing plugin codes with `wrapperPlugins`, the driver profile can be set with this parameter.

Example: See [below](#configuration-profiles). | `null` | To use a built-in plugin, specify its relevant plugin code for the `wrapperPlugins`. The default value for `wrapperPlugins` is `auroraConnectionTracker,failover,efm`. These 3 plugins are enabled by default. To read more about these plugins, see the [List of Available Plugins](#list-of-available-plugins) section. @@ -86,20 +95,46 @@ properties.setProperty("wrapperPlugins", ""); The Wrapper behaves like the target driver when no plugins are used. ### Configuration Profiles -An alternative way of loading plugins is to use a configuration profile. You can create custom configuration profiles that specify which plugins the AWS JDBC Driver should load. After creating the profile, set the [`wrapperProfileName`](#connection-plugin-manager-parameters) parameter to the name of the created profile. -Although you can use this method of loading plugins, this method will most often be used by those who require custom plugins that cannot be loaded with the [`wrapperPlugins`](#connection-plugin-manager-parameters) parameter. +An alternative way of loading plugins and providing configuration parameters is to use a configuration profile. You can create custom configuration profiles that specify which plugins the AWS JDBC Driver should load. After creating the profile, set the [`wrapperProfileName`](#connection-plugin-manager-parameters) parameter to the name of the created profile. +This method of loading plugins will most often be used by those who require custom plugins that cannot be loaded with the [`wrapperPlugins`](#connection-plugin-manager-parameters) parameter, or by those who are using preset configurations. + +Besides a list of plugins to load and configuration properties, configuration profiles may also include the following items: +- [Database Dialect](./using-the-jdbc-driver/DatabaseDialects.md#database-dialects) +- [Target Driver Dialect](./using-the-jdbc-driver/TargetDriverDialects.md#target-driver-dialects) +- a custom exception handler +- a custom connection provider + The following example creates and sets a configuration profile: ```java -properties.setProperty("wrapperProfileName", "testProfile"); -DriverConfigurationProfiles.addOrReplaceProfile( - "testProfile", - Arrays.asList( - FailoverConnectionPluginFactory.class, +// Create a new configuration profile with name "testProfile" +ConfigurationProfileBuilder.get() + .withName("testProfile") + .withPluginFactories(Arrays.asList( + FailoverConnectionPluginFactory.class, HostMonitoringConnectionPluginFactory.class, - CustomConnectionPluginFactory.class)); + CustomConnectionPluginFactory.class)) + .buildAndSet(); + +// Use the configuration profile "testProfile" +properties.setProperty("wrapperProfileName", "testProfile"); ``` +Configuration profiles can be created based on other existing configuration profiles. Profile names are case sensitive and should be unique. + +```java +// Create a new configuration profile with name "newProfile" based on "existingProfileName" +ConfigurationProfileBuilder.from("existingProfileName") + .withName("newProfileName") + .withDialect(new CustomDatabaseDialect()) +.buildAndSet(); + +// Delete configuration profile "testProfile" +DriverConfigurationProfiles.remove("testProfile"); +``` + +The AWS JDBC Driver team has gathered and analyzed various user scenarios to create commonly used configuration profiles, or presets, for users. These preset configuration profiles are optimized, profiled, verified and can be used right away. Users can create their own configuration profiles based on the built-in presets as shown above. More details could be found at the [Configuration Presets](./ConfigurationPresets.md) page. + ### Executing Custom Code When Initializing a Connection In some use cases you may need to define a specific configuration for a new driver connection before your application can use it. For instance: - you might need to run some initial SQL queries when a connection is established, or; @@ -125,20 +160,21 @@ ConnectionProviderManager.setConnectionInitFunc((connection, protocol, hostSpec, ### List of Available Plugins The AWS JDBC Driver has several built-in plugins that are available to use. Please visit the individual plugin page for more details. -| Plugin name | Plugin Code | Database Compatibility | Description | Additional Required Dependencies | -|------------------------------------------------------------------------------------------------|---------------------------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Failover Connection Plugin](./using-plugins/UsingTheFailoverPlugin.md) | `failover` | Aurora, RDS Multi-AZ DB Cluster | Enables the failover functionality supported by Amazon Aurora clusters and RDS Multi-AZ DB clusters. Prevents opening a wrong connection to an old writer node dues to stale DNS after failover event. This plugin is enabled by default. | None | -| [Host Monitoring Connection Plugin](./using-plugins/UsingTheHostMonitoringPlugin.md) | `efm` | Aurora, RDS Multi-AZ DB Cluster | Enables enhanced host connection failure monitoring, allowing faster failure detection rates. This plugin is enabled by default. | None | -| Data Cache Connection Plugin | `dataCache` | Any database | Caches results from SQL queries matching the regular expression specified in the `dataCacheTriggerCondition` configuration parameter. | None | -| Execution Time Connection Plugin | `executionTime` | Any database | Logs the time taken to execute any JDBC method. | None | -| Log Query Connection Plugin | `logQuery` | Any database | Tracks and logs the SQL statements to be executed. Sometimes SQL statements are not passed directly to the JDBC method as a parameter, such as [executeBatch()](https://docs.oracle.com/javase/8/docs/api/java/sql/Statement.html#executeBatch--). Users can set `enhancedLogQueryEnabled` to `true`, allowing the JDBC Wrapper to obtain SQL statements via Java Reflection.

:warning:**Note:** Enabling Java Reflection may cause a performance degradation. | None | -| [IAM Authentication Connection Plugin](./using-plugins/UsingTheIamAuthenticationPlugin.md) | `iam` | Any database | Enables users to connect to their Amazon Aurora clusters using AWS Identity and Access Management (IAM). | [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) | -| [AWS Secrets Manager Connection Plugin](./using-plugins/UsingTheAwsSecretsManagerPlugin.md) | `awsSecretsManager` | Any database | Enables fetching database credentials from the AWS Secrets Manager service. | [Jackson Databind](https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind)
[AWS Secrets Manager](https://central.sonatype.com/artifact/software.amazon.awssdk/secretsmanager) | -| Aurora Stale DNS Plugin | `auroraStaleDns` | Aurora | Prevents incorrectly opening a new connection to an old writer node when DNS records have not yet updated after a recent failover event.

:warning:**Note:** Contrary to `failover` plugin, `auroraStaleDns` plugin doesn't implement failover support itself. It helps to eliminate opening wrong connections to an old writer node after cluster failover is completed.

:warning:**Note:** This logic is already included in `failover` plugin so you can omit using both plugins at the same time. | None | -| [Aurora Connection Tracker Plugin](./using-plugins/UsingTheAuroraConnectionTrackerPlugin.md) | `auroraConnectionTracker` | Aurora, RDS Multi-AZ DB Cluster | Tracks all the opened connections. In the event of a cluster failover, the plugin will close all the impacted connections to the node. This plugin is enabled by default. | None | -| [Driver Metadata Connection Plugin](./using-plugins/UsingTheDriverMetadataConnectionPlugin.md) | `driverMetaData` | Any database | Allows user application to override the return value of `DatabaseMetaData#getDriverName` | None | -| [Read Write Splitting Plugin](./using-plugins/UsingTheReadWriteSplittingPlugin.md) | `readWriteSplitting` | Aurora | Enables read write splitting functionality where users can switch between database reader and writer instances. | None | -| [Developer Plugin](./using-plugins/UsingTheDeveloperPlugin.md) | `dev` | Any database | Helps developers test various everyday scenarios including rare events like network outages and database cluster failover. The plugin allows injecting and raising an expected exception, then verifying how applications handle it. | None | +| Plugin name | Plugin Code | Database Compatibility | Description | Additional Required Dependencies | +|-------------------------------------------------------------------------------------------------------------------|---------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Failover Connection Plugin](./using-plugins/UsingTheFailoverPlugin.md) | `failover` | Aurora, RDS Multi-AZ DB Cluster | Enables the failover functionality supported by Amazon Aurora clusters and RDS Multi-AZ DB clusters. Prevents opening a wrong connection to an old writer node dues to stale DNS after failover event. This plugin is enabled by default. | None | +| [Host Monitoring Connection Plugin](./using-plugins/UsingTheHostMonitoringPlugin.md) | `efm` | Aurora, RDS Multi-AZ DB Cluster | Enables enhanced host connection failure monitoring, allowing faster failure detection rates. This plugin is enabled by default. | None | +| [Host Monitoring Connection Plugin v2](./using-plugins/UsingTheHostMonitoringPlugin.md#host-monitoring-plugin-v2) | `efm2` | Aurora, RDS Multi-AZ DB Cluster | Enables enhanced host connection failure monitoring, allowing faster failure detection rates. This plugin is an alternative implementation for host health status monitoring. It is functionally the same as the `efm` plugin and uses the same configuration parameters. This plugin is experimental. | None | +| Data Cache Connection Plugin | `dataCache` | Any database | Caches results from SQL queries matching the regular expression specified in the `dataCacheTriggerCondition` configuration parameter. | None | +| Execution Time Connection Plugin | `executionTime` | Any database | Logs the time taken to execute any JDBC method. | None | +| Log Query Connection Plugin | `logQuery` | Any database | Tracks and logs the SQL statements to be executed. Sometimes SQL statements are not passed directly to the JDBC method as a parameter, such as [executeBatch()](https://docs.oracle.com/javase/8/docs/api/java/sql/Statement.html#executeBatch--). Users can set `enhancedLogQueryEnabled` to `true`, allowing the JDBC Wrapper to obtain SQL statements via Java Reflection.

:warning:**Note:** Enabling Java Reflection may cause a performance degradation. | None | +| [IAM Authentication Connection Plugin](./using-plugins/UsingTheIamAuthenticationPlugin.md) | `iam` | Any database | Enables users to connect to their Amazon Aurora clusters using AWS Identity and Access Management (IAM). | [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) | +| [AWS Secrets Manager Connection Plugin](./using-plugins/UsingTheAwsSecretsManagerPlugin.md) | `awsSecretsManager` | Any database | Enables fetching database credentials from the AWS Secrets Manager service. | [Jackson Databind](https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind)
[AWS Secrets Manager](https://central.sonatype.com/artifact/software.amazon.awssdk/secretsmanager) | +| Aurora Stale DNS Plugin | `auroraStaleDns` | Aurora | Prevents incorrectly opening a new connection to an old writer node when DNS records have not yet updated after a recent failover event.

:warning:**Note:** Contrary to `failover` plugin, `auroraStaleDns` plugin doesn't implement failover support itself. It helps to eliminate opening wrong connections to an old writer node after cluster failover is completed.

:warning:**Note:** This logic is already included in `failover` plugin so you can omit using both plugins at the same time. | None | +| [Aurora Connection Tracker Plugin](./using-plugins/UsingTheAuroraConnectionTrackerPlugin.md) | `auroraConnectionTracker` | Aurora, RDS Multi-AZ DB Cluster | Tracks all the opened connections. In the event of a cluster failover, the plugin will close all the impacted connections to the node. This plugin is enabled by default. | None | +| [Driver Metadata Connection Plugin](./using-plugins/UsingTheDriverMetadataConnectionPlugin.md) | `driverMetaData` | Any database | Allows user application to override the return value of `DatabaseMetaData#getDriverName` | None | +| [Read Write Splitting Plugin](./using-plugins/UsingTheReadWriteSplittingPlugin.md) | `readWriteSplitting` | Aurora | Enables read write splitting functionality where users can switch between database reader and writer instances. | None | +| [Developer Plugin](./using-plugins/UsingTheDeveloperPlugin.md) | `dev` | Any database | Helps developers test various everyday scenarios including rare events like network outages and database cluster failover. The plugin allows injecting and raising an expected exception, then verifying how applications handle it. | None | :exclamation: **NOTE**: As an enhancement, the wrapper is now able to automatically set the Aurora host list provider for connections to Aurora MySQL and Aurora PostgreSQL databases. Aurora Host List Connection Plugin is deprecated. If you were using the Aurora Host List Connection Plugin, you can simply remove the plugin from the `wrapperPlugins` parameter. @@ -159,7 +195,7 @@ If there is an unreleased feature you would like to try, it may be available in software.amazon.jdbc aws-advanced-jdbc-wrapper - 2.3.1-SNAPSHOT + 2.3.2-SNAPSHOT system path-to-snapshot-jar @@ -175,9 +211,9 @@ dependencies { ## AWS JDBC Driver for MySQL Migration Guide -**[The Amazon Web Services (AWS) JDBC Driver for MySQL](https://github.com/awslabs/aws-mysql-jdbc)**allows an +**[The Amazon Web Services (AWS) JDBC Driver for MySQL](https://github.com/awslabs/aws-mysql-jdbc)** allows an application to take advantage of the features of clustered MySQL databases. It is based on and can be used as a drop-in -compatible for the[MySQL Connector/J driver](https://github.com/mysql/mysql-connector-j), and is compatible with all +compatible for the [MySQL Connector/J driver](https://github.com/mysql/mysql-connector-j), and is compatible with all MySQL deployments. The AWS JDBC Driver has the same functionalities as the AWS JDBC Driver for MySQL, as well as additional features such as support for Read/Write Splitting. This diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md index 3ad3e5d5b..9a3c05e60 100644 --- a/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md +++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md @@ -30,7 +30,7 @@ In addition to the parameters that you can configure for the underlying driver, | `failoverReaderConnectTimeoutMs` | Integer | No | Maximum allowed time in milliseconds to attempt to connect to a reader instance during a reader failover process. | `30000` | | `failoverTimeoutMs` | Integer | No | Maximum allowed time in milliseconds to attempt reconnecting to a new writer or reader instance after a cluster failover is initiated. | `300000` | | `failoverWriterReconnectIntervalMs` | Integer | No | Interval of time in milliseconds to wait between attempts to reconnect to a failed writer during a writer failover process. | `2000` | -| `keepSessionStateOnFailover` | Boolean | No | This parameter will allow connections to retain the session state after failover. When keepSessionStateOnFailover is set to false, connections will need to be reconfigured as seen in the example [here](./../../../examples/AWSDriverExample/src/main/java/software/amazon/PgFailoverSample.java). When this parameter is true, the autocommit and readOnly values will be kept. This parameter is only necessary when the session state must be retained and the connection cannot be manually reconfigured by the user.

**Please note:** this parameter will not be able to fully restore the connection session state, as it will only save the autocommit and readOnly values. | `false` | +| ~~`keepSessionStateOnFailover`~~ | Boolean | No | This parameter is no longer available. If specified, it will be ignored by the driver. See [Session State](../SessionState.md) for more details. | `false` | | ~~`enableFailoverStrictReader`~~ | Boolean | No | This parameter is no longer available and, if specified, it will be ignored by the driver. See `failoverMode` (`reader-or-writer` or `strict-reader`) for more details. | | ## Host Pattern diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheFederatedAuthPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheFederatedAuthPlugin.md new file mode 100644 index 000000000..cd07b8dd1 --- /dev/null +++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheFederatedAuthPlugin.md @@ -0,0 +1,49 @@ +# Federated Authentication Plugin + +The Federated Authentication Plugin adds support for authentication via Federated Identity and then database access via IAM. +Currently, only Microsoft Active Directory Federation Services (AD FS) is supported. + +## What is Federated Identity +Federated Identity allows users to use the same set of credentials to access multiple services or resources across different organizations. This works by having Identity Providers (IdP) that manage and authenticate user credentials, and Service Providers (SP) that are services or resources that can be internal, external, and/or belonging to various organizations. Multiple SPs can establish trust relationships with a single IdP. + +When a user wants access to a resource, it authenticates with the IdP. From this a security token generated and is passed to the SP then grants access to said resource. +In the case of AD FS, the user signs into the AD FS sign in page. This generates a SAML Assertion which acts as a security token. The user then passes the SAML Assertion to the SP when requesting access to resources. The SP verifies the SAML Assertion and grants access to the user. + +## Prerequisites +> [!WARNING] +> To preserve compatibility with customers using the community driver, this plugin requires the [AWS Java SDK RDS v2.7.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) and the [AWS Java SDK STS v2.7.x](https://central.sonatype.com/artifact/software.amazon.awssdk/sts) to be included separately in the classpath. The AWS Java SDK RDS and AWS Java SDK STS are runtime dependencies and must be resolved. + +## How to use the Federated Authentication Plugin with the AWS JDBC Driver + +### Enabling the Federated Authentication Plugin +Note: AWS IAM database authentication is needed to use the Federated Authentication Plugin. This is because after the plugin acquires the authentication token (ex. SAML Assertion in the case of AD FS), the authentication token is then used to acquire an AWS IAM token. The AWS IAM token is then subsequently used to access the database. + +1. Enable AWS IAM database authentication on an existing database or create a new database with AWS IAM database authentication on the AWS RDS Console: + - If needed, review the documentation about [IAM authentication for MariaDB, MySQL, and PostgreSQL](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html). +2. Set up an IAM Identity Provider and IAM role. The IAM role should be using the IAM policy set up in step 1. + - If needed, review the documentation about [creating IAM identity providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create.html). For AD FS, see the documention about [creating IAM SAML identity providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml.html). +3. Add the plugin code `federatedAuth` to the [`wrapperPlugins`](../UsingTheJdbcDriver.md#connection-plugin-manager-parameters) value, or to the current [driver profile](../UsingTheJdbcDriver.md#connection-plugin-manager-parameters). +4. Specify parameters that are required or specific to your case. + +### Federated Authentication Plugin Parameters +| Parameter | Value | Required | Description | Default Value | Example Value | +|----------------------------|:-------:|:--------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------|--------------------------------------------------------| +| `dbUser` | String | Yes | The user name of the IAM user with access to your database.
If you have previously used the IAM Authentication Plugin, this would be the same IAM user.
For information on how to connect to your Aurora Database with IAM, see this [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/UsingWithRDS.IAMDBAuth.Connecting.html). | `null` | `some_user_name` | +| `idpUsername` | String | Yes | The user name for the `idpEndpoint` server. If this parameter is not specified, the plugin will fallback to using the `user` parameter. | `null` | `jimbob@example.com` | +| `idpPassword` | String | Yes | The password associated with the `idpEndpoint` username. If this parameter is not specified, the plugin will fallback to using the `password` parameter. | `null` | `someRandomPassword` | +| `idpEndpoint` | String | Yes | The hosting URL for the service that you are using to authenticate into AWS Aurora. | `null` | `ec2amaz-ab3cdef.example.com` | +| `iamRoleArn` | String | Yes | The ARN of the IAM Role that is to be assumed to access AWS Aurora. | `null` | `arn:aws:iam::123456789012:role/adfs_example_iam_role` | +| `iamIdpArn` | String | Yes | The ARN of the Identity Provider. | `null` | `arn:aws:iam::123456789012:saml-provider/adfs_example` | +| `iamRegion` | String | Yes | The IAM region where the IAM token is generated. | `null` | `us-east-2` | +| `idpName` | String | No | The name of the Identity Provider implementation used. | `adfs` | `adfs` | +| `idpPort` | String | No | The port that the host for the authentication service listens at. | `443` | `1234` | +| `rpIdentifier` | String | No | The relaying party identifier. | `urn:amazon:webservices` | `urn:amazon:webservices` | +| `iamHost` | String | No | Overrides the host that is used to generate the IAM token. | `null` | `database.cluster-hash.us-east-1.rds.amazonaws.com` | +| `iamDefaultPort` | String | No | This property overrides the default port that is used to generate the IAM token. The default port is determined based on the underlying driver protocol. For now, there is support for `jdbc:postgresql:` and `jdbc:mysql:`. Target drivers with different protocols will require users to provide a default port. | `null` | `1234` | +| `iamTokenExpiration` | Integer | No | Overrides the default IAM token cache expiration in seconds | `930` | `123` | +| `httpClientSocketTimeout` | Integer | No | The socket timeout value in milliseconds for the HttpClient used by the FederatedAuthenticationPlugin. | `60000` | `60000` | +| `httpClientConnectTimeout` | Integer | No | The connect timeout value in milliseconds for the HttpClient used by the FederatedAuthenticationPlugin. | `60000` | `60000` | +| `sslInsecure` | Boolean | No | Indicates whether or not the SSL connection is secure or not. If not, it will allow SSL connections to be made without validating the server's certificates. | `true` | `false` | + +## Sample code +[FederatedAuthPluginExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/FederatedAuthPluginExample.java) diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md index e52f3d6a2..c658088c9 100644 --- a/docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md +++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheHostMonitoringPlugin.md @@ -72,3 +72,21 @@ properties.setProperty("monitoring-socketTimeout", "10"); > We recommend you either disable the Host Monitoring Connection Plugin or avoid using RDS Proxy endpoints when the Host Monitoring Connection Plugin is active. > > Although using RDS Proxy endpoints with the AWS Advanced JDBC Driver with Enhanced Failure Monitoring doesn't cause any critical issues, we don't recommend this approach. The main reason is that RDS Proxy transparently re-routes requests to a single database instance. RDS Proxy decides which database instance is used based on many criteria (on a per-request basis). Switching between different instances makes the Host Monitoring Connection Plugin useless in terms of instance health monitoring because the plugin will be unable to identify which instance it's connected to, and which one it's monitoring. This could result in false positive failure detections. At the same time, the plugin will still proactively monitor network connectivity to RDS Proxy endpoints and report outages back to a user application if they occur. + +# **Experimental** Host Monitoring Plugin v2 + +> [!WARNING] This plugin is experimental and users should test the plugin before using it in production environment. + +Host Monitoring Plugin v2, also known as `efm2`, is an alternative implementation of enhanced failure monitoring and it is functionally equal to the Host Monitoring Plugin described above. Both plugins share the same set of [configuration parameters](#enhanced-failure-monitoring-parameters). The `efm2` plugin is designed to be a drop-in replacement for the `efm` plugin. +The `efm2` plugin can be used in any scenario where the `efm` plugin is mentioned. + +> [!NOTE] Since these two plugins are separate plugins, users may decide to use them together with a single connection. While this should not have any negative side effects, it is not recommended. It is recommended to use either the `efm` plugin, or the `efm2` plugin where it's needed. + + +The `efm2` plugin is designed to address [some of the issues](https://github.com/awslabs/aws-advanced-jdbc-wrapper/issues/675) that have been reported by multiple users. The following changes have been made: +- Used weak pointers to ease garbage collection +- Split monitoring logic into two separate threads to increase overall monitoring stability +- Reviewed locks for monitoring context +- Reviewed and redesigned stopping of idle monitoring threads +- Reviewed and simplified monitoring logic + diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md index 7290a1be1..0c4e42f84 100644 --- a/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md +++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheReadWriteSplittingPlugin.md @@ -1,17 +1,17 @@ -## Read-Write Splitting Plugin +# Read/Write Splitting Plugin -The read-write splitting plugin adds functionality to switch between writer/reader instances via calls to the `Connection#setReadOnly` method. Upon calling `setReadOnly(true)`, the plugin will establish a connection to a reader instance and direct subsequent queries to this instance. Future calls to `setReadOnly` will switch between the established writer and reader connections according to the boolean argument you supply to the `setReadOnly` method. +The read/write splitting plugin adds functionality to switch between writer/reader instances via calls to the `Connection#setReadOnly` method. Upon calling `setReadOnly(true)`, the plugin will connect to a reader instance according to a [reader selection strategy](#reader-selection-strategies) and direct subsequent queries to this instance. Future calls to `setReadOnly` will switch between the established writer and reader connections according to the boolean argument you supply to the `setReadOnly` method. -### Loading the Read-Write Splitting Plugin +## Loading the Read/Write Splitting Plugin -The read-write splitting plugin is not loaded by default. To load the plugin, include it in the `wrapperPlugins` connection parameter. If you would like to load the read-write splitting plugin alongside the failover and host monitoring plugins, the read-write splitting plugin must be listed before these plugins in the plugin chain. If it is not, failover exceptions will not be properly processed by the plugin. See the example below to properly load the read-write splitting plugin with these plugins. +The read/write splitting plugin is not loaded by default. To load the plugin, include it in the `wrapperPlugins` connection parameter. If you would like to load the read/write splitting plugin alongside the failover and host monitoring plugins, the read/write splitting plugin must be listed before these plugins in the plugin chain. If it is not, failover exceptions will not be properly processed by the plugin. See the example below to properly load the read/write splitting plugin with these plugins. ``` final Properties properties = new Properties(); properties.setProperty(PropertyDefinition.PLUGINS.name, "readWriteSplitting,failover,efm"); ``` -If you would like to use the read-write splitting plugin without the failover plugin, make sure you have the `readWriteSplitting` plugin in the `wrapperPlugins` property, and that the failover plugin is not part of it. +If you would like to use the read/write splitting plugin without the failover plugin, make sure you have the `readWriteSplitting` plugin in the `wrapperPlugins` property, and that the failover plugin is not part of it. ``` final Properties properties = new Properties(); properties.setProperty(PropertyDefinition.PLUGINS.name, "readWriteSplitting"); @@ -19,25 +19,27 @@ properties.setProperty(PropertyDefinition.PLUGINS.name, "readWriteSplitting"); > The Aurora Host List Plugin is deprecated after version 2.2.3. To use the Read Write Splitting plugin without failover with versions 2.2.3 and earlier, add the Aurora Host List Plugin to the plugin list like so: `"auroraHostList,readWriteSplitting"`. -### Supplying the connection string +## Supplying the connection string -When using the read-write splitting plugin against Aurora clusters, you do not have to supply multiple instance URLs in the connection string. Instead, supply just the URL for the initial instance to which you're connecting. You must also include either the failover plugin or the Aurora host list plugin in your plugin chain so that the driver knows to query Aurora for its topology. See the section on [loading the read-write splitting plugin](#loading-the-read-write-splitting-plugin) for more info. +When using the read/write splitting plugin against Aurora clusters, you do not have to supply multiple instance URLs in the connection string. Instead, supply just the URL for the initial instance to which you're connecting. You must also include either the failover plugin or the Aurora host list plugin in your plugin chain so that the driver knows to query Aurora for its topology. See the section on [loading the read/write splitting plugin](#loading-the-readwrite-splitting-plugin) for more info. -### Using the Read-Write Splitting Plugin against non-Aurora clusters +## Using the Read/Write Splitting Plugin against non-Aurora clusters -The read-write splitting plugin is not currently supported for non-Aurora clusters. +The read/write splitting plugin is not currently supported for non-Aurora clusters. -### Internal connection pooling +## Internal connection pooling -> :warning: If internal connection pools are enabled, database passwords may not be verified with every connection request. The initial connection request for each database instance in the cluster will verify the password, but subsequent requests may return a cached pool connection without re-verifying the password. This behavior is inherent to the nature of connection pools in general and not a bug with the driver. `ConnectionProviderManager.releaseResources` can be called to close all pools and remove all cached pool connections. See [InternalConnectionPoolPasswordWarning.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/InternalConnectionPoolPasswordWarning.java) for more details. +> [!WARNING]\ +> If internal connection pools are enabled, database passwords may not be verified with every connection request. The initial connection request for each database instance in the cluster will verify the password, but subsequent requests may return a cached pool connection without re-verifying the password. This behavior is inherent to the nature of connection pools in general and not a bug with the driver. `ConnectionProviderManager.releaseResources` can be called to close all pools and remove all cached pool connections. See [InternalConnectionPoolPasswordWarning.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/InternalConnectionPoolPasswordWarning.java) for more details. -Whenever `setReadOnly(true)` is first called on a `Connection` object, the read-write plugin will internally open a new physical connection to a reader. After this first call, the physical reader connection will be cached for the given `Connection`. Future calls to `setReadOnly `on the same `Connection` object will not require opening a new physical connection. However, calling `setReadOnly(true)` for the first time on a new `Connection` object will require the plugin to establish another new physical connection to a reader. If your application frequently calls `setReadOnly`, you can enable internal connection pooling to improve performance. When enabled, the wrapper driver will maintain an internal connection pool for each instance in the cluster. This allows the read-write plugin to reuse connections that were established by `setReadOnly` calls on previous `Connection` objects. +Whenever `setReadOnly(true)` is first called on a `Connection` object, the read/write plugin will internally open a new physical connection to a reader. After this first call, the physical reader connection will be cached for the given `Connection`. Future calls to `setReadOnly `on the same `Connection` object will not require opening a new physical connection. However, calling `setReadOnly(true)` for the first time on a new `Connection` object will require the plugin to establish another new physical connection to a reader. If your application frequently calls `setReadOnly`, you can enable internal connection pooling to improve performance. When enabled, the wrapper driver will maintain an internal connection pool for each instance in the cluster. This allows the read/write plugin to reuse connections that were established by `setReadOnly` calls on previous `Connection` objects. -> Note: Initial connections to a cluster URL will not be pooled. The driver does not pool cluster URLs because it can be problematic to pool a URL that resolves to different instances over time. The main benefit of internal connection pools is when setReadOnly is called. When setReadOnly is called (regardless of the initial connection URL), an internal pool will be created for the writer/reader that the plugin switches to and connections for that instance can be reused in the future. +> [!NOTE]\ +> Initial connections to a cluster URL will not be pooled. The driver does not pool cluster URLs because it can be problematic to pool a URL that resolves to different instances over time. The main benefit of internal connection pools is when setReadOnly is called. When setReadOnly is called (regardless of the initial connection URL), an internal pool will be created for the writer/reader that the plugin switches to and connections for that instance can be reused in the future. The wrapper driver currently uses [Hikari](https://github.com/brettwooldridge/HikariCP) to create and maintain its internal connection pools. The sample code [here](../../../examples/AWSDriverExample/src/main/java/software/amazon/ReadWriteSplittingPostgresExample.java) provides a useful example of how to enable this feature. The steps are as follows: -1. Create an instance of `HikariPooledConnectionProvider`. The `HikariPooledConnectionProvider` constructor requires you to pass in a `HikariPoolConfigurator` function. Inside this function, you should create a `HikariConfig`, configure any desired properties on it, and return it. Note that the Hikari properties below will be set by default and will override any values you set in your function. This is done to follow desired behavior and ensure that the read-write plugin can internally establish connections to new instances. +1. Create an instance of `HikariPooledConnectionProvider`. The `HikariPooledConnectionProvider` constructor requires you to pass in a `HikariPoolConfigurator` function. Inside this function, you should create a `HikariConfig`, configure any desired properties on it, and return it. Note that the Hikari properties below will be set by default and will override any values you set in your function. This is done to follow desired behavior and ensure that the read/write plugin can internally establish connections to new instances. - jdbcUrl (including the host, port, and database) - exception override class name @@ -46,7 +48,8 @@ The wrapper driver currently uses [Hikari](https://github.com/brettwooldridge/Hi You can optionally pass in a `HikariPoolMapping` function as a second parameter to the `HikariPooledConnectionProvider`. This allows you to decide when new connection pools should be created by defining what is included in the pool map key. A new pool will be created each time a new connection is requested with a unique key. By default, a new pool will be created for each unique instance-user combination. If you would like to define a different key system, you should pass in a `HikariPoolMapping` function defining this logic. A simple example is show below. Please see [ReadWriteSplittingPostgresExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/ReadWriteSplittingPostgresExample.java) for the full example. -> :warning: If you do not include the username in your HikariPoolMapping function, connection pools may be shared between different users. As a result, an initial connection established with a privileged user may be returned to a connection request with a lower-privilege user without re-verifying credentials. This behavior is inherent to the nature of connection pools in general and not a bug with the driver. `ConnectionProviderManager.releaseResources` can be called to close all pools and remove all cached pool connections. +> [!WARNING]\ +> If you do not include the username in your HikariPoolMapping function, connection pools may be shared between different users. As a result, an initial connection established with a privileged user may be returned to a connection request with a lower-privilege user without re-verifying credentials. This behavior is inherent to the nature of connection pools in general and not a bug with the driver. `ConnectionProviderManager.releaseResources` can be called to close all pools and remove all cached pool connections. ```java props.setProperty("somePropertyValue", "1"); // used in getPoolKey @@ -69,43 +72,44 @@ private static String getPoolKey(HostSpec hostSpec, Properties props) { 2. Call `ConnectionProviderManager.setConnectionProvider`, passing in the `HikariPooledConnectionProvider` you created in step 1. -3. By default, the read-write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. If you would like the plugin to select a reader based on a different connection strategy, please see the [Connection Strategies](#connection-strategies) section for more information. +3. By default, the read/write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. If you would like the plugin to select a reader based on a different selection strategy, please see the [Reader Selection Strategies](#reader-selection-strategies) section for more information. 4. Continue as normal: create connections and use them as needed. 5. When you are finished using all connections, call `ConnectionProviderManager.releaseResources`. -> :warning: **Note:** You must call `ConnectionProviderManager.releaseResources` to close the internal connection pools when you are finished using all connections. Unless `ConnectionProviderManager.releaseResources` is called, the wrapper driver will keep the pools open so that they can be shared between connections. +> [!IMPORTANT]\ +> You must call `ConnectionProviderManager.releaseResources` to close the internal connection pools when you are finished using all connections. Unless `ConnectionProviderManager.releaseResources` is called, the wrapper driver will keep the pools open so that they can be shared between connections. -### Example -[ReadWriteSplittingPostgresExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/ReadWriteSplittingPostgresExample.java) demonstrates how to enable and configure read-write splitting with the Aws Advanced JDBC Driver. +## Example +[ReadWriteSplittingPostgresExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/ReadWriteSplittingPostgresExample.java) demonstrates how to enable and configure read/write splitting with the Aws Advanced JDBC Driver. -### Connection Strategies -By default, the read-write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. To balance connections to reader instances more evenly, different connection strategies can be used. The following table describes the currently available connection strategies and any relevant configuration parameters for each strategy. +## Reader Selection Strategies +By default, the read/write plugin randomly selects a reader instance the first time that `setReadOnly(true)` is called. To balance connections to reader instances more evenly, different selection strategies can be used. The following table describes the currently available selection strategies and any relevant configuration parameters for each strategy. -To indicate which connection strategy to use, the `readerHostSelectorStrategy` configuration parameter can be set to one of the connection strategies in the table below. The following is an example of enabling the least connections strategy: +To indicate which selection strategy to use, the `readerHostSelectorStrategy` configuration parameter can be set to one of the selection strategies in the table below. The following is an example of enabling the least connections strategy: ```java props.setProperty(ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.name, "leastConnections"); ``` -| Connection Strategy | Configuration Parameter | Description | Default Value | -|---------------------|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| -| `random` | This strategy does not have configuration parameters. | The random strategy is the default connection strategy. When switching to a reader connection, the reader instance will be chosen randomly from the available database instances. | N/A | -| `leastConnections` | This strategy does not have configuration parameters. | The least connections strategy will select reader instances based on which database instance has the least number of currently active connections. Note that this strategy is only available when internal connection pools are enabled - if you set the connection property without enabling internal pools, an exception will be thrown. | N/A | -| `roundRobin` | See the following rows for configuration parameters. | The round robin strategy will select a reader instance by taking turns with all available database instances in a cycle. A slight addition to the round robin strategy is the weighted round robin strategy, where more connections will be passed to reader instances based on user specified connection properties. | N/A | -| | `roundRobinHostWeightPairs` | This parameter value must be a `string` type comma separated list of database host-weight pairs in the format `:`. The host represents the database instance name, and the weight represents how many connections should be directed to the host in one cycle through all available hosts. For example, the value `instance-1:1,instance-2:4` means that for every connection to `instance-1`, there will be four connections to `instance-2`.

**Note:** The `` value in the string must be an integer greater than or equal to 1. | `null` | -| | `roundRobinDefaultWeight` | This parameter value must be an integer value in the form of a `string`. This parameter represents the default weight for any hosts that have not been configured with the `roundRobinHostWeightPairs` parameter. For example, if a connection were already established and host weights were set with `roundRobinHostWeightPairs` but a new reader node was added to the database, the new reader node would use the default weight.

**Note:** This value must be an integer greater than or equal to 1. | `1` | +| Reader Selection Strategy | Configuration Parameter | Description | Default Value | +|---------------------------|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| `random` | This strategy does not have configuration parameters. | The random strategy is the default selection strategy. When switching to a reader connection, the reader instance will be chosen randomly from the available database instances. | N/A | +| `leastConnections` | This strategy does not have configuration parameters. | The least connections strategy will select reader instances based on which database instance has the least number of currently active connections. Note that this strategy is only available when internal connection pools are enabled - if you set the connection property without enabling internal pools, an exception will be thrown. | N/A | +| `roundRobin` | See the following rows for configuration parameters. | The round robin strategy will select a reader instance by taking turns with all available database instances in a cycle. A slight addition to the round robin strategy is the weighted round robin strategy, where more connections will be passed to reader instances based on user specified connection properties. | N/A | +| | `roundRobinHostWeightPairs` | This parameter value must be a `string` type comma separated list of database host-weight pairs in the format `:`. The host represents the database instance name, and the weight represents how many connections should be directed to the host in one cycle through all available hosts. For example, the value `instance-1:1,instance-2:4` means that for every connection to `instance-1`, there will be four connections to `instance-2`.

**Note:** The `` value in the string must be an integer greater than or equal to 1. | `null` | +| | `roundRobinDefaultWeight` | This parameter value must be an integer value in the form of a `string`. This parameter represents the default weight for any hosts that have not been configured with the `roundRobinHostWeightPairs` parameter. For example, if a connection were already established and host weights were set with `roundRobinHostWeightPairs` but a new reader node was added to the database, the new reader node would use the default weight.

**Note:** This value must be an integer greater than or equal to 1. | `1` | -### Limitations +## Limitations -#### General plugin limitations +### General plugin limitations -When a Statement or ResultSet is created, it is internally bound to the database connection established at that moment. There is no standard JDBC functionality to change the internal connection used by Statement or ResultSet objects. Consequently, even if the read-write plugin switches the internal connection, any Statements/ResultSets created before this will continue using the old database connection. This bypasses the desired functionality provided by the plugin. To prevent these scenarios, an exception will be thrown if your code uses any Statements/ResultSets created before a change in internal connection. To solve this problem, please ensure you create new Statement/ResultSet objects after switching between the writer/reader. +When a Statement or ResultSet is created, it is internally bound to the database connection established at that moment. There is no standard JDBC functionality to change the internal connection used by Statement or ResultSet objects. Consequently, even if the read/write plugin switches the internal connection, any Statements/ResultSets created before this will continue using the old database connection. This bypasses the desired functionality provided by the plugin. To prevent these scenarios, an exception will be thrown if your code uses any Statements/ResultSets created before a change in internal connection. To solve this problem, please ensure you create new Statement/ResultSet objects after switching between the writer/reader. -#### Session state limitations +### Session state limitations -There are many session state attributes that can change during a session, and many ways to change them. Consequently, the read-write splitting plugin has limited support for transferring session state between connections. The following attributes will be automatically transferred when switching connections: +There are many session state attributes that can change during a session, and many ways to change them. Consequently, the read/write splitting plugin has limited support for transferring session state between connections. The following attributes will be automatically transferred when switching connections: - autocommit value - transaction isolation level @@ -113,3 +117,17 @@ There are many session state attributes that can change during a session, and ma All other session state attributes will be lost when switching connections between the writer/reader. If your SQL workflow depends on session state attributes that are not mentioned above, you will need to re-configure those attributes each time that you switch between the writer/reader. + + +### Limitations when using Spring Boot/Framework + +#### @Transactional(readOnly = True) + +> [!WARNING]\ +> The use of read/write splitting with the annotation @Transactional(readOnly = True) is not recommended. + +When a method with this annotation is hit, Spring calls conn.setReadOnly(true), executes the method, and then calls setReadOnly(false) to restore the connection's initial readOnly value. Consequently, every time the method is called, the plugin switches to the reader, executes the method, and then switches back to the writer. Although the reader connection will be cached after the first setReadOnly call, there is still some overhead when switching between the cached writer/reader connections. This constant switching is not an ideal use of the plugin because it is frequently incurring this overhead. The suggested approach for this scenario is to avoid loading the read/write splitting plugin and instead use the writer cluster URL for your write operations and the reader cluster URL for your read operations. By doing this you avoid the overhead of constantly switching between connections while still spreading load across the database instances in your cluster. + +#### Internal connection pools + +We recommend that you do not enable internal connection pools when using Spring. This is because Spring by default uses its own external connection pool. The use of both internal and external pools is not tested and may result in problematic behavior. diff --git a/examples/AWSDriverExample/build.gradle.kts b/examples/AWSDriverExample/build.gradle.kts index fa1bc972a..b097f8da1 100644 --- a/examples/AWSDriverExample/build.gradle.kts +++ b/examples/AWSDriverExample/build.gradle.kts @@ -16,14 +16,15 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-jdbc:2.7.13") // 2.7.13 is the last version compatible with Java 8 - implementation("org.postgresql:postgresql:42.6.0") + implementation("org.postgresql:postgresql:42.7.1") implementation("mysql:mysql-connector-java:8.0.33") - implementation("software.amazon.awssdk:rds:2.21.11") - implementation("software.amazon.awssdk:secretsmanager:2.21.21") - implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + implementation("software.amazon.awssdk:rds:2.22.13") + implementation("software.amazon.awssdk:secretsmanager:2.22.5") + implementation("software.amazon.awssdk:sts:2.22.13") + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") implementation(project(":aws-advanced-jdbc-wrapper")) - implementation("io.opentelemetry:opentelemetry-api:1.31.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.31.0") - implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0") - implementation("com.amazonaws:aws-xray-recorder-sdk-core:2.14.0") + implementation("io.opentelemetry:opentelemetry-api:1.33.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.33.0") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.33.0") + implementation("com.amazonaws:aws-xray-recorder-sdk-core:2.15.0") } diff --git a/examples/AWSDriverExample/src/main/java/software/amazon/FederatedAuthPluginExample.java b/examples/AWSDriverExample/src/main/java/software/amazon/FederatedAuthPluginExample.java new file mode 100644 index 000000000..d59747a0c --- /dev/null +++ b/examples/AWSDriverExample/src/main/java/software/amazon/FederatedAuthPluginExample.java @@ -0,0 +1,53 @@ +/* + * 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; + +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.plugin.federatedauth.FederatedAuthPlugin; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +public class FederatedAuthPluginExample { + + private static final String CONNECTION_STRING = "jdbc:aws-wrapper:postgresql://db-identifier.XYZ.us-east-2.rds.amazonaws.com:5432/employees"; + + public static void main(String[] args) throws SQLException { + // Set the AWS Federated Authentication Connection Plugin parameters and the JDBC Wrapper parameters. + final Properties properties = new Properties(); + + // Enable the AWS Federated Authentication Connection Plugin. + properties.setProperty(PropertyDefinition.PLUGINS.name, "federatedAuth"); + properties.setProperty(FederatedAuthPlugin.IDP_ENDPOINT.name, "ec2amaz-ab3cdef.example.com"); + properties.setProperty(FederatedAuthPlugin.IAM_ROLE_ARN.name, "arn:aws:iam::123456789012:role/adfs_example_iam_role"); + properties.setProperty(FederatedAuthPlugin.IAM_IDP_ARN.name, "arn:aws:iam::123456789012:saml-provider/adfs_example"); + properties.setProperty(FederatedAuthPlugin.IAM_REGION.name, "us-east-2"); + properties.setProperty(FederatedAuthPlugin.IDP_USERNAME.name, "someFederatedUsername@example.com"); + properties.setProperty(FederatedAuthPlugin.IDP_PASSWORD.name, "somePassword"); + properties.setProperty(FederatedAuthPlugin.DB_USER.name, "someIamUser"); + + // Try and make a connection: + try (final Connection conn = DriverManager.getConnection(CONNECTION_STRING, properties); + final Statement statement = conn.createStatement(); + final ResultSet rs = statement.executeQuery("SELECT 1")) { + System.out.println(Util.getResult(rs)); + } + } +} diff --git a/examples/HikariExample/build.gradle.kts b/examples/HikariExample/build.gradle.kts index 2f868c29f..4d1d9534d 100644 --- a/examples/HikariExample/build.gradle.kts +++ b/examples/HikariExample/build.gradle.kts @@ -15,7 +15,7 @@ */ dependencies { - implementation("org.postgresql:postgresql:42.6.0") + implementation("org.postgresql:postgresql:42.7.1") implementation("mysql:mysql-connector-java:8.0.33") implementation(project(":aws-advanced-jdbc-wrapper")) implementation("com.zaxxer:HikariCP:4.0.3") diff --git a/examples/HikariExample/src/main/java/software/amazon/HikariExample.java b/examples/HikariExample/src/main/java/software/amazon/HikariExample.java index 44799ef11..9bbfa03c9 100644 --- a/examples/HikariExample/src/main/java/software/amazon/HikariExample.java +++ b/examples/HikariExample/src/main/java/software/amazon/HikariExample.java @@ -47,6 +47,12 @@ public static void main(String[] args) throws SQLException { ds.addDataSourceProperty("serverPort", "5432"); ds.addDataSourceProperty("serverName", ENDPOINT); + // Alternatively, the AwsWrapperDataSource can be configured with a JDBC URL instead of individual properties as + // seen above. + ds.addDataSourceProperty( + "jdbcUrl", + "jdbc:aws-wrapper:postgresql://db-identifier.cluster-XYZ.us-east-2.rds.amazonaws.com:5432/postgres"); + // Specify the driver-specific data source for AwsWrapperDataSource: ds.addDataSourceProperty("targetDataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); diff --git a/examples/HikariExample/src/main/java/software/amazon/HikariFailoverExample.java b/examples/HikariExample/src/main/java/software/amazon/HikariFailoverExample.java index d1ce43c12..308df2745 100644 --- a/examples/HikariExample/src/main/java/software/amazon/HikariFailoverExample.java +++ b/examples/HikariExample/src/main/java/software/amazon/HikariFailoverExample.java @@ -50,6 +50,12 @@ public static void main(String[] args) throws SQLException { ds.addDataSourceProperty("serverPort", "5432"); ds.addDataSourceProperty("database", DATABASE_NAME); + // Alternatively, the AwsWrapperDataSource can be configured with a JDBC URL instead of individual properties as + // seen above. + ds.addDataSourceProperty( + "jdbcUrl", + "jdbc:aws-wrapper:postgresql://db-identifier.cluster-XYZ.us-east-2.rds.amazonaws.com:5432/postgres"); + // The failover plugin throws failover-related exceptions that need to be handled explicitly by HikariCP, // otherwise connections will be closed immediately after failover. Set `ExceptionOverrideClassName` to provide // a custom exception class. diff --git a/examples/ReadWriteSplittingSample/build.gradle.kts b/examples/ReadWriteSplittingSample/build.gradle.kts new file mode 100644 index 000000000..f27061925 --- /dev/null +++ b/examples/ReadWriteSplittingSample/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +dependencies { + implementation("org.postgresql:postgresql:42.7.1") + implementation("mysql:mysql-connector-java:8.0.33") + implementation("com.zaxxer:HikariCP:4.0.3") + implementation(project(":aws-advanced-jdbc-wrapper")) +} + +tasks.withType { + systemProperty("java.util.logging.config.file", "${project.buildDir}/resources/main/logging.properties") +} diff --git a/examples/ReadWriteSplittingSample/gradle.properties b/examples/ReadWriteSplittingSample/gradle.properties new file mode 100644 index 000000000..dc802102f --- /dev/null +++ b/examples/ReadWriteSplittingSample/gradle.properties @@ -0,0 +1,16 @@ +# 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. + +# Do not publish the Jar file for this subproject +nexus.publish=false diff --git a/examples/ReadWriteSplittingSample/src/main/java/software/amazon/ReadWriteSplittingSample.java b/examples/ReadWriteSplittingSample/src/main/java/software/amazon/ReadWriteSplittingSample.java new file mode 100644 index 000000000..ac1c2a772 --- /dev/null +++ b/examples/ReadWriteSplittingSample/src/main/java/software/amazon/ReadWriteSplittingSample.java @@ -0,0 +1,267 @@ +/* + * 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; + +import com.zaxxer.hikari.HikariConfig; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import software.amazon.jdbc.ConnectionProviderManager; +import software.amazon.jdbc.HikariPooledConnectionProvider; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin; + +public class ReadWriteSplittingSample { + // Adjust this value to run the sample with different driver configurations: + // 1: no r/w splitting + // 2: r/w splitting using r/w plugin + // 3: r/w plugin with internal connection pools + static final int APPROACH_ID = 1; + static final int NUM_THREADS = 250; + static final int NUM_WRITES = 2500; + static final int NUM_READS = 5000; + static final int MAX_SIMULTANEOUS = 40; + static final int THREAD_DELAY_MS = 2000; + static final int EXECUTOR_TIMEOUT_MINS = 90; + static final boolean DELETE_TABLES_ON_STARTUP = true; + static final String WRITER_CLUSTER = + "jdbc:aws-wrapper:postgresql://test-db.cluster-XYZ.us-east-2.rds.amazonaws.com/readWriteSplittingSample"; + static final String USER = "username"; + static final String PASSWORD = "password"; + static final Semaphore sem = new Semaphore(MAX_SIMULTANEOUS); + static final Logger LOGGER = Logger.getLogger(ReadWriteSplittingSample.class.getName()); + static final Properties noRwProps; + static final Properties rwProps; + static final Properties poolProps; + + static { + noRwProps = new Properties(); + noRwProps.setProperty(PropertyDefinition.USER.name, USER); + noRwProps.setProperty(PropertyDefinition.PASSWORD.name, PASSWORD); + + rwProps = new Properties(); + rwProps.setProperty(PropertyDefinition.PLUGINS.name, "readWriteSplitting"); + rwProps.setProperty(PropertyDefinition.USER.name, USER); + rwProps.setProperty(PropertyDefinition.PASSWORD.name, PASSWORD); + + poolProps = new Properties(); + poolProps.setProperty(ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.name, "leastConnections"); + poolProps.setProperty(PropertyDefinition.PLUGINS.name, "readWriteSplitting"); + poolProps.setProperty(PropertyDefinition.USER.name, USER); + poolProps.setProperty(PropertyDefinition.PASSWORD.name, PASSWORD); + } + + public static void main(String[] args) throws SQLException { + LOGGER.info(String.format( + "Approach ID: %d, Total threads: %d, Writes per thread: %d, Reads per thread: %d, Max simultaneous threads: %d, Thread delay: %d", + APPROACH_ID, NUM_THREADS, NUM_WRITES, NUM_READS, MAX_SIMULTANEOUS, THREAD_DELAY_MS)); + + Properties props; + if (APPROACH_ID == 1) { + props = noRwProps; + } else if (APPROACH_ID == 2) { + props = rwProps; + } else if (APPROACH_ID == 3) { + props = poolProps; + } else { + throw new RuntimeException( + String.format( + "The approach ID should be set to a value between 1 and 3 (inclusive). Detected value: %d", APPROACH_ID)); + } + + if (DELETE_TABLES_ON_STARTUP) { + deleteTables(); + } + + long start = System.nanoTime(); + if (APPROACH_ID == 3) { + LOGGER.info("Enabling internal connection pools..."); + final HikariPooledConnectionProvider provider = + new HikariPooledConnectionProvider(ReadWriteSplittingSample::getHikariConfig); + ConnectionProviderManager.setConnectionProvider(provider); + } + + final ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS); + try { + for (int i = 0; i < NUM_THREADS; i++) { + if (APPROACH_ID == 1) { + executorService.submit(new NoRWSplittingThread(i)); + } else { + executorService.submit(new RWSplittingThread(i, props)); // RWThread should be used for approach 2 and approach 3. + } + + if (i < MAX_SIMULTANEOUS) { + // Space out initial threads to distribute workload across time. + TimeUnit.MILLISECONDS.sleep(THREAD_DELAY_MS); + } + } + + executorService.shutdown(); + LOGGER.info("Waiting for threads to complete..."); + boolean successfullyTerminated = executorService.awaitTermination(EXECUTOR_TIMEOUT_MINS, TimeUnit.MINUTES); + if (!successfullyTerminated) { + LOGGER.warning(String.format( + "The executor service timed out after waiting %d minutes for termination. " + + "Consider increasing the EXECUTOR_TIMEOUT_MINS value.", EXECUTOR_TIMEOUT_MINS)); + } + + if (APPROACH_ID == 3) { + LOGGER.info("Closing internal connection pools..."); + ConnectionProviderManager.releaseResources(); + } + + long duration = System.nanoTime() - start; + LOGGER.info(String.format("Test completed in %dms", TimeUnit.NANOSECONDS.toMillis(duration))); + } catch (InterruptedException e) { + LOGGER.severe("The main thread was interrupted."); + throw new RuntimeException(e); + } finally { + deleteTables(); + } + } + + private static void deleteTables() throws SQLException { + try (Connection conn = DriverManager.getConnection(WRITER_CLUSTER, noRwProps); + Statement stmt = conn.createStatement()) { + for (int i = 0; i < NUM_THREADS; i++) { + String dropTableSql = String.format("drop table if exists rw_sample_%s", i); + stmt.addBatch(dropTableSql); + } + stmt.executeBatch(); + } + } + + private static HikariConfig getHikariConfig(HostSpec hostSpec, Properties props) { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(10); + return config; + } + + private static void executeWrites(Connection conn, int tableNum) throws SQLException { + long start = System.nanoTime(); + + String createSql = String.format("create table rw_sample_%s(some_num int not null)", tableNum); + String insertSql = String.format("insert into rw_sample_%s values (1)", tableNum); + try (Statement stmt = conn.createStatement()) { + stmt.execute(createSql); + + for (int i = 0; i < NUM_WRITES; i++) { + stmt.addBatch(insertSql); + } + stmt.executeBatch(); + } + + long duration = System.nanoTime() - start; + long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); + LOGGER.finest(String.format("Thread %d write duration: %dms", tableNum, durationMs)); + } + + private static void executeReads(Connection conn, int tableNum) throws SQLException { + long start = System.nanoTime(); + String selectSQL = String.format("select * from rw_sample_%s", tableNum); + + try (Statement stmt = conn.createStatement()) { + for (int i = 0; i < NUM_READS; i++) { + stmt.execute(selectSQL); + } + } + + long duration = System.nanoTime() - start; + long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); + LOGGER.finest(String.format("Thread %d read duration: %dms", tableNum, durationMs)); + } + + static class NoRWSplittingThread implements Callable { + private final int id; + + NoRWSplittingThread(int id) { + this.id = id; + } + + @Override + public Void call() throws SQLException, InterruptedException { + sem.acquire(); + try { + long start = System.nanoTime(); + try (Connection conn = DriverManager.getConnection(WRITER_CLUSTER, noRwProps)) { + long duration = System.nanoTime() - start; + long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); + LOGGER.finest(String.format("Thread %d connect duration: %dms", this.id, durationMs)); + + executeWrites(conn, this.id); + executeReads(conn, this.id); + } + } catch (SQLException e) { + LOGGER.severe(String.format("Thread %d encountered SQLException: %s", this.id, e.getMessage())); + throw e; + } finally { + sem.release(); + } + + return null; + } + } + + static class RWSplittingThread implements Callable { + private final int id; + private final Properties props; + + RWSplittingThread(int id, Properties props) { + this.id = id; + this.props = props; + } + + @Override + public Void call() throws SQLException, InterruptedException { + sem.acquire(); + try { + long start = System.nanoTime(); + try (Connection conn = DriverManager.getConnection(WRITER_CLUSTER, this.props)) { + long duration = System.nanoTime() - start; + long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); + LOGGER.finest(String.format("Thread %d connect duration: %dms", this.id, durationMs)); + + executeWrites(conn, this.id); + + start = System.nanoTime(); + conn.setReadOnly(true); + duration = System.nanoTime() - start; + durationMs = TimeUnit.NANOSECONDS.toMillis(duration); + LOGGER.finest(String.format("Thread %d switch to reader duration: %dms", this.id, durationMs)); + + executeReads(conn, this.id); + } + } catch (SQLException e) { + LOGGER.severe(String.format("Thread %d encountered SQLException: %s", this.id, e.getMessage())); + throw e; + } finally { + sem.release(); + } + + return null; + } + } +} diff --git a/examples/ReadWriteSplittingSample/src/main/resources/logging.properties b/examples/ReadWriteSplittingSample/src/main/resources/logging.properties new file mode 100644 index 000000000..6f3a78eaf --- /dev/null +++ b/examples/ReadWriteSplittingSample/src/main/resources/logging.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +# Possible values for log level (from most detailed to less detailed): FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE +.level=INFO +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=ALL + +software.amazon.ReadWriteSplittingSample.level=INFO +software.amazon.jdbc.level=INFO diff --git a/examples/SpringBootHikariExample/README.md b/examples/SpringBootHikariExample/README.md index b9e95976c..63f79f8e9 100644 --- a/examples/SpringBootHikariExample/README.md +++ b/examples/SpringBootHikariExample/README.md @@ -4,7 +4,7 @@ In this tutorial, you will set up a Spring Boot application using Hikari and the > Note: this tutorial was written using the following technologies: > - Spring Boot 2.7.0 -> - AWS JDBC Driver 2.3.0 +> - AWS JDBC Driver 2.3.2 > - Postgresql 42.5.4 > - Java 8 diff --git a/examples/SpringBootHikariExample/build.gradle.kts b/examples/SpringBootHikariExample/build.gradle.kts index a2518e2d3..0cd4b692d 100644 --- a/examples/SpringBootHikariExample/build.gradle.kts +++ b/examples/SpringBootHikariExample/build.gradle.kts @@ -22,7 +22,7 @@ plugins { dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jdbc") implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.postgresql:postgresql:42.6.0") + implementation("org.postgresql:postgresql:42.7.1") implementation(project(":aws-advanced-jdbc-wrapper")) } diff --git a/examples/SpringHibernateExample/README.md b/examples/SpringHibernateExample/README.md index 03da71039..6b8807123 100644 --- a/examples/SpringHibernateExample/README.md +++ b/examples/SpringHibernateExample/README.md @@ -5,7 +5,7 @@ In this tutorial, you will set up a Spring Boot and Hibernate application with t > Note: this tutorial was written using the following technologies: > - Spring Boot 2.7.1 > - Hibernate -> - AWS Advanced JDBC Driver 2.3.0 +> - AWS JDBC Driver 2.3.2 > - Postgresql 42.5.4 > - Gradle 7 > - Java 11 diff --git a/examples/SpringHibernateExample/build.gradle.kts b/examples/SpringHibernateExample/build.gradle.kts index d6add053b..161b67807 100644 --- a/examples/SpringHibernateExample/build.gradle.kts +++ b/examples/SpringHibernateExample/build.gradle.kts @@ -22,7 +22,7 @@ plugins { dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.postgresql:postgresql:42.6.0") - implementation("software.amazon.awssdk:rds:2.21.11") + implementation("org.postgresql:postgresql:42.7.1") + implementation("software.amazon.awssdk:rds:2.22.13") implementation(project(":aws-advanced-jdbc-wrapper")) } diff --git a/examples/SpringTxFailoverExample/README.md b/examples/SpringTxFailoverExample/README.md index f801b96a3..f2593222d 100644 --- a/examples/SpringTxFailoverExample/README.md +++ b/examples/SpringTxFailoverExample/README.md @@ -4,7 +4,7 @@ In this tutorial, you will set up a Spring Boot application using the AWS JDBC D > Note: this tutorial was written using the following technologies: > - Spring Boot 2.7.0 -> - AWS JDBC Driver 2.3.0 +> - AWS JDBC Driver 2.3.2 > - Postgresql 42.5.4 > - Java 8 diff --git a/examples/SpringWildflyExample/README.md b/examples/SpringWildflyExample/README.md index 3b10077e1..fb7b8d3d1 100644 --- a/examples/SpringWildflyExample/README.md +++ b/examples/SpringWildflyExample/README.md @@ -5,7 +5,7 @@ In this tutorial, you will set up a Wildfly and Spring Boot application with the > Note: this tutorial was written using the following technologies: > - Spring Boot 2.7.1 > - Wildfly 26.1.1 Final -> - AWS JDBC Driver 2.3.0 +> - AWS JDBC Driver 2.3.2 > - Postgresql 42.5.4 > - Gradle 7 > - Java 11 @@ -38,7 +38,7 @@ Create a Gradle project with the following project hierarchy: │ └───main │ │ │───module.xml │ │ │───postgresql-42.5.4.jar - │ │ └───aws-advanced-jdbc-wrapper-2.3.0.jar + │ │ └───aws-advanced-jdbc-wrapper-2.3.2.jar └───standalone ├───configuration ├───amazon @@ -135,7 +135,7 @@ Since this example uses the PostgreSQL JDBC driver as the target driver, you nee - + diff --git a/examples/SpringWildflyExample/spring/build.gradle.kts b/examples/SpringWildflyExample/spring/build.gradle.kts index 053d7ef3c..533e5ce10 100644 --- a/examples/SpringWildflyExample/spring/build.gradle.kts +++ b/examples/SpringWildflyExample/spring/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-jdbc") implementation("org.springframework.boot:spring-boot-starter-web") runtimeOnly("org.springframework.boot:spring-boot-devtools") - implementation("org.postgresql:postgresql:42.6.0") - implementation("software.amazon.awssdk:rds:2.21.11") + implementation("org.postgresql:postgresql:42.7.1") + implementation("software.amazon.awssdk:rds:2.22.13") implementation(project(":aws-advanced-jdbc-wrapper")) } diff --git a/examples/SpringWildflyExample/wildfly/modules/software/amazon/jdbc/main/module.xml b/examples/SpringWildflyExample/wildfly/modules/software/amazon/jdbc/main/module.xml index 9c4e557f4..deaee294b 100644 --- a/examples/SpringWildflyExample/wildfly/modules/software/amazon/jdbc/main/module.xml +++ b/examples/SpringWildflyExample/wildfly/modules/software/amazon/jdbc/main/module.xml @@ -19,7 +19,7 @@ - + diff --git a/examples/VertxExample/README.md b/examples/VertxExample/README.md index d2c18bf56..3ccc65541 100644 --- a/examples/VertxExample/README.md +++ b/examples/VertxExample/README.md @@ -3,7 +3,7 @@ In this tutorial, you will set up a Vert.x application with the AWS JDBC Driver, and use the driver to execute some simple database operations on an Aurora PostgreSQL database. > Note: this tutorial was written using the following technologies: -> - AWS JDBC Driver 2.3.0 +> - AWS JDBC Driver 2.3.2 > - PostgreSQL 42.5.4 > - Java 8 > - Vert.x 4.4.2 diff --git a/examples/VertxExample/build.gradle.kts b/examples/VertxExample/build.gradle.kts index e99e4b3a0..de1823437 100644 --- a/examples/VertxExample/build.gradle.kts +++ b/examples/VertxExample/build.gradle.kts @@ -33,13 +33,13 @@ application { } dependencies { - implementation(platform("io.vertx:vertx-stack-depchain:4.4.6")) + implementation(platform("io.vertx:vertx-stack-depchain:4.5.1")) implementation("io.vertx:vertx-core") implementation("io.vertx:vertx-config") implementation("io.vertx:vertx-jdbc-client") implementation("io.vertx:vertx-web") - implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") - implementation("org.postgresql:postgresql:42.6.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") + implementation("org.postgresql:postgresql:42.7.1") implementation(project(":aws-advanced-jdbc-wrapper")) } diff --git a/gradle.properties b/gradle.properties index 74b0f0eb9..6336848c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,6 @@ aws-advanced-jdbc-wrapper.version.major=2 aws-advanced-jdbc-wrapper.version.minor=3 -aws-advanced-jdbc-wrapper.version.subminor=0 +aws-advanced-jdbc-wrapper.version.subminor=2 snapshot=false nexus.publish=true diff --git a/settings.gradle.kts b/settings.gradle.kts index c43fda35c..869230bdc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,8 @@ include( "springwildfly", "springboothikariexample", "springtxfailover", - "vertxexample" + "vertxexample", + "readwritesample" ) project(":aws-advanced-jdbc-wrapper").projectDir = file("wrapper") @@ -38,6 +39,7 @@ project(":springwildfly").projectDir = file("examples/SpringWildflyExample/sprin project(":springboothikariexample").projectDir = file("examples/SpringBootHikariExample") project(":springtxfailover").projectDir = file("examples/SpringTxFailoverExample") project(":vertxexample").projectDir = file("examples/VertxExample") +project(":readwritesample").projectDir = file("examples/ReadWriteSplittingSample") pluginManagement { plugins { @@ -45,7 +47,7 @@ pluginManagement { fun PluginDependenciesSpec.idv(id: String, key: String = id) = id(id) version key.v() id("biz.aQute.bnd.builder") version "6.4.0" - id("com.github.spotbugs") version "5.2.+" + id("com.github.spotbugs") version "6.0.+" id("com.diffplug.spotless") version "6.13.0" // 6.13.0 is the last version that is compatible with Java 8 id("com.github.vlsi.gradle-extensions") version "1.+" id("com.github.vlsi.stage-vote-release") version "1.+" diff --git a/wrapper/build.gradle.kts b/wrapper/build.gradle.kts index 227d6357f..df355caf0 100644 --- a/wrapper/build.gradle.kts +++ b/wrapper/build.gradle.kts @@ -28,19 +28,21 @@ plugins { dependencies { implementation("org.checkerframework:checker-qual:3.40.0") - compileOnly("software.amazon.awssdk:rds:2.21.11") + compileOnly("org.apache.httpcomponents:httpclient:4.5.14") + compileOnly("software.amazon.awssdk:rds:2.22.13") + compileOnly("software.amazon.awssdk:sts:2.22.13") compileOnly("com.zaxxer:HikariCP:4.0.3") // Version 4.+ is compatible with Java 8 - compileOnly("software.amazon.awssdk:secretsmanager:2.21.21") - compileOnly("com.fasterxml.jackson.core:jackson-databind:2.15.3") + compileOnly("software.amazon.awssdk:secretsmanager:2.22.5") + compileOnly("com.fasterxml.jackson.core:jackson-databind:2.16.1") compileOnly("mysql:mysql-connector-java:8.0.33") - compileOnly("org.postgresql:postgresql:42.6.0") - compileOnly("org.mariadb.jdbc:mariadb-java-client:3.3.0") + compileOnly("org.postgresql:postgresql:42.7.1") + compileOnly("org.mariadb.jdbc:mariadb-java-client:3.3.1") compileOnly("org.osgi:org.osgi.core:6.0.0") compileOnly("org.osgi:org.osgi.core:6.0.0") - compileOnly("com.amazonaws:aws-xray-recorder-sdk-core:2.14.0") - compileOnly("io.opentelemetry:opentelemetry-api:1.31.0") - compileOnly("io.opentelemetry:opentelemetry-sdk:1.31.0") - compileOnly("io.opentelemetry:opentelemetry-sdk-metrics:1.31.0") + compileOnly("com.amazonaws:aws-xray-recorder-sdk-core:2.15.0") + compileOnly("io.opentelemetry:opentelemetry-api:1.33.0") + compileOnly("io.opentelemetry:opentelemetry-sdk:1.33.0") + compileOnly("io.opentelemetry:opentelemetry-sdk-metrics:1.33.0") testImplementation("org.junit.platform:junit-platform-commons:1.10.1") @@ -48,34 +50,35 @@ dependencies { testImplementation("org.junit.platform:junit-platform-launcher:1.10.1") testImplementation("org.junit.platform:junit-platform-suite-engine:1.10.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testImplementation("org.apache.commons:commons-dbcp2:2.11.0") - testImplementation("org.postgresql:postgresql:42.6.0") + testImplementation("org.postgresql:postgresql:42.7.1") testImplementation("mysql:mysql-connector-java:8.0.33") - testImplementation("org.mariadb.jdbc:mariadb-java-client:3.3.0") + testImplementation("org.mariadb.jdbc:mariadb-java-client:3.3.1") testImplementation("com.zaxxer:HikariCP:4.0.3") // Version 4.+ is compatible with Java 8 testImplementation("org.springframework.boot:spring-boot-starter-jdbc:2.7.13") // 2.7.13 is the last version compatible with Java 8 testImplementation("org.mockito:mockito-inline:4.11.0") // 4.11.0 is the last version compatible with Java 8 - testImplementation("software.amazon.awssdk:rds:2.21.11") - testImplementation("software.amazon.awssdk:ec2:2.21.12") - testImplementation("software.amazon.awssdk:secretsmanager:2.21.21") - testImplementation("org.testcontainers:testcontainers:1.19.1") - testImplementation("org.testcontainers:mysql:1.19.1") - testImplementation("org.testcontainers:postgresql:1.19.1") - testImplementation("org.testcontainers:mariadb:1.19.1") - testImplementation("org.testcontainers:junit-jupiter:1.19.1") - testImplementation("org.testcontainers:toxiproxy:1.19.1") + testImplementation("software.amazon.awssdk:rds:2.22.13") + testImplementation("software.amazon.awssdk:ec2:2.22.9") + testImplementation("software.amazon.awssdk:secretsmanager:2.22.5") + testImplementation("software.amazon.awssdk:sts:2.22.13") + testImplementation("org.testcontainers:testcontainers:1.19.3") + testImplementation("org.testcontainers:mysql:1.19.3") + testImplementation("org.testcontainers:postgresql:1.19.3") + testImplementation("org.testcontainers:mariadb:1.19.3") + testImplementation("org.testcontainers:junit-jupiter:1.19.3") + testImplementation("org.testcontainers:toxiproxy:1.19.3") testImplementation("eu.rekawek.toxiproxy:toxiproxy-java:2.1.7") - testImplementation("org.apache.poi:poi-ooxml:5.2.4") + testImplementation("org.apache.poi:poi-ooxml:5.2.5") testImplementation("org.slf4j:slf4j-simple:2.0.9") - testImplementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") - testImplementation("com.amazonaws:aws-xray-recorder-sdk-core:2.14.0") - testImplementation("io.opentelemetry:opentelemetry-api:1.31.0") - testImplementation("io.opentelemetry:opentelemetry-sdk:1.31.0") - testImplementation("io.opentelemetry:opentelemetry-sdk-metrics:1.31.0") - testImplementation("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") + testImplementation("com.amazonaws:aws-xray-recorder-sdk-core:2.15.0") + testImplementation("io.opentelemetry:opentelemetry-api:1.33.0") + testImplementation("io.opentelemetry:opentelemetry-sdk:1.33.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-metrics:1.33.0") + testImplementation("io.opentelemetry:opentelemetry-exporter-otlp:1.33.0") } repositories { @@ -250,7 +253,9 @@ tasks.withType { outputs.upToDateWhen { false } System.getProperties().forEach { - if (it.key.toString().startsWith("test-no-")) { + if (it.key.toString().startsWith("test-no-") + || it.key.toString() == "test-include-tags" + || it.key.toString() == "test-exclude-tags") { systemProperty(it.key.toString(), it.value.toString()) } } @@ -391,6 +396,10 @@ tasks.register("test-all-aurora-performance") { systemProperty("test-no-hikari", "true") systemProperty("test-no-secrets-manager", "true") systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-exclude-tags", "advanced,rw-splitting") } } @@ -403,10 +412,34 @@ tasks.register("test-aurora-pg-performance") { systemProperty("test-no-hikari", "true") systemProperty("test-no-secrets-manager", "true") systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") systemProperty("test-no-mysql-driver", "true") systemProperty("test-no-mysql-engine", "true") systemProperty("test-no-mariadb-driver", "true") systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-exclude-tags", "advanced,rw-splitting") + } +} + +tasks.register("debug-aurora-pg-performance") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.debugTests") + doFirst { + systemProperty("test-no-docker", "true") + systemProperty("test-no-iam", "true") + systemProperty("test-no-hikari", "true") + systemProperty("test-no-secrets-manager", "true") + systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") + systemProperty("test-no-mysql-driver", "true") + systemProperty("test-no-mysql-engine", "true") + systemProperty("test-no-mariadb-driver", "true") + systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-exclude-tags", "advanced,rw-splitting") } } @@ -419,10 +452,74 @@ tasks.register("test-aurora-mysql-performance") { systemProperty("test-no-hikari", "true") systemProperty("test-no-secrets-manager", "true") systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") + systemProperty("test-no-pg-driver", "true") + systemProperty("test-no-pg-engine", "true") + systemProperty("test-no-mariadb-driver", "true") + systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-exclude-tags", "advanced,rw-splitting") + } +} + +tasks.register("debug-aurora-mysql-performance") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.debugTests") + doFirst { + systemProperty("test-no-docker", "true") + systemProperty("test-no-iam", "true") + systemProperty("test-no-hikari", "true") + systemProperty("test-no-secrets-manager", "true") + systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") + systemProperty("test-no-pg-driver", "true") + systemProperty("test-no-pg-engine", "true") + systemProperty("test-no-mariadb-driver", "true") + systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-exclude-tags", "advanced,rw-splitting") + } +} + +tasks.register("test-aurora-pg-advanced-performance") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("test-no-docker", "true") + systemProperty("test-no-iam", "true") + systemProperty("test-no-hikari", "true") + systemProperty("test-no-secrets-manager", "true") + systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") + systemProperty("test-no-mysql-driver", "true") + systemProperty("test-no-mysql-engine", "true") + systemProperty("test-no-mariadb-driver", "true") + systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-include-tags", "advanced") + } +} + +tasks.register("test-aurora-mysql-advanced-performance") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("test-no-docker", "true") + systemProperty("test-no-iam", "true") + systemProperty("test-no-hikari", "true") + systemProperty("test-no-secrets-manager", "true") + systemProperty("test-no-graalvm", "true") + systemProperty("test-no-openjdk8", "true") systemProperty("test-no-pg-driver", "true") systemProperty("test-no-pg-engine", "true") systemProperty("test-no-mariadb-driver", "true") systemProperty("test-no-mariadb-engine", "true") + systemProperty("test-no-instances-1", "true") + systemProperty("test-no-instances-2", "true") + systemProperty("test-include-tags", "advanced") } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java index 96997f14d..b82d866b0 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java @@ -25,8 +25,10 @@ import java.util.Properties; import java.util.logging.Logger; import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.plugin.AuroraConnectionTrackerPluginFactory; import software.amazon.jdbc.plugin.AuroraHostListConnectionPluginFactory; +import software.amazon.jdbc.plugin.AuroraInitialConnectionStrategyPluginFactory; import software.amazon.jdbc.plugin.AwsSecretsManagerConnectionPluginFactory; import software.amazon.jdbc.plugin.ConnectTimeConnectionPluginFactory; import software.amazon.jdbc.plugin.DataCacheConnectionPluginFactory; @@ -38,9 +40,11 @@ import software.amazon.jdbc.plugin.dev.DeveloperConnectionPluginFactory; import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPluginFactory; import software.amazon.jdbc.plugin.failover.FailoverConnectionPluginFactory; +import software.amazon.jdbc.plugin.federatedauth.FederatedAuthPluginFactory; import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPluginFactory; import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsPluginFactory; -import software.amazon.jdbc.profile.DriverConfigurationProfiles; +import software.amazon.jdbc.plugin.strategy.fastestresponse.FastestResponseStrategyPluginFactory; +import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlState; import software.amazon.jdbc.util.StringUtils; @@ -60,15 +64,19 @@ public class ConnectionPluginChainBuilder { put("logQuery", LogQueryConnectionPluginFactory.class); put("dataCache", DataCacheConnectionPluginFactory.class); put("efm", HostMonitoringConnectionPluginFactory.class); + put("efm2", software.amazon.jdbc.plugin.efm2.HostMonitoringConnectionPluginFactory.class); put("failover", FailoverConnectionPluginFactory.class); put("iam", IamAuthConnectionPluginFactory.class); put("awsSecretsManager", AwsSecretsManagerConnectionPluginFactory.class); + put("federatedAuth", FederatedAuthPluginFactory.class); put("auroraStaleDns", AuroraStaleDnsPluginFactory.class); put("readWriteSplitting", ReadWriteSplittingPluginFactory.class); put("auroraConnectionTracker", AuroraConnectionTrackerPluginFactory.class); put("driverMetaData", DriverMetaDataConnectionPluginFactory.class); put("connectTime", ConnectTimeConnectionPluginFactory.class); put("dev", DeveloperConnectionPluginFactory.class); + put("fastestResponseStrategy", FastestResponseStrategyPluginFactory.class); + put("initialConnection", AuroraInitialConnectionStrategyPluginFactory.class); } }; @@ -83,14 +91,18 @@ public class ConnectionPluginChainBuilder { put(DriverMetaDataConnectionPluginFactory.class, 100); put(DataCacheConnectionPluginFactory.class, 200); put(AuroraHostListConnectionPluginFactory.class, 300); + put(AuroraInitialConnectionStrategyPluginFactory.class, 390); put(AuroraConnectionTrackerPluginFactory.class, 400); put(AuroraStaleDnsPluginFactory.class, 500); put(ReadWriteSplittingPluginFactory.class, 600); put(FailoverConnectionPluginFactory.class, 700); put(HostMonitoringConnectionPluginFactory.class, 800); - put(IamAuthConnectionPluginFactory.class, 900); - put(AwsSecretsManagerConnectionPluginFactory.class, 1000); - put(LogQueryConnectionPluginFactory.class, 1100); + put(software.amazon.jdbc.plugin.efm2.HostMonitoringConnectionPluginFactory.class, 810); + put(FastestResponseStrategyPluginFactory.class, 900); + put(IamAuthConnectionPluginFactory.class, 1000); + put(AwsSecretsManagerConnectionPluginFactory.class, 1100); + put(FederatedAuthPluginFactory.class, 1200); + put(LogQueryConnectionPluginFactory.class, 1300); put(ConnectTimeConnectionPluginFactory.class, WEIGHT_RELATIVE_TO_PRIOR_PLUGIN); put(ExecutionTimeConnectionPluginFactory.class, WEIGHT_RELATIVE_TO_PRIOR_PLUGIN); put(DeveloperConnectionPluginFactory.class, WEIGHT_RELATIVE_TO_PRIOR_PLUGIN); @@ -116,25 +128,17 @@ public PluginFactoryInfo(final Class factory, public List getPlugins( final PluginService pluginService, final ConnectionProvider defaultConnProvider, + final ConnectionProvider effectiveConnProvider, final PluginManagerService pluginManagerService, - final Properties props) + final Properties props, + @Nullable ConfigurationProfile configurationProfile) throws SQLException { List plugins; List> pluginFactories; - final String profileName = PropertyDefinition.PROFILE_NAME.getString(props); - - if (profileName != null) { - - if (!DriverConfigurationProfiles.contains(profileName)) { - throw new SQLException( - Messages.get( - "ConnectionPluginManager.configurationProfileNotFound", - new Object[] {profileName})); - } - pluginFactories = DriverConfigurationProfiles.getPluginFactories(profileName); - + if (configurationProfile != null && configurationProfile.getPluginFactories() != null) { + pluginFactories = configurationProfile.getPluginFactories(); } else { String pluginCodes = PropertyDefinition.PLUGINS.getString(props); @@ -194,8 +198,12 @@ public List getPlugins( } // add default connection plugin to the tail - final ConnectionPlugin defaultPlugin = - new DefaultConnectionPlugin(pluginService, defaultConnProvider, pluginManagerService); + final ConnectionPlugin defaultPlugin = new DefaultConnectionPlugin( + pluginService, + defaultConnProvider, + effectiveConnProvider, + pluginManagerService); + plugins.add(defaultPlugin); return plugins; diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java index d65aa2fcd..fb78ccfa6 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java @@ -33,6 +33,7 @@ import software.amazon.jdbc.cleanup.CanReleaseResources; import software.amazon.jdbc.plugin.AuroraConnectionTrackerPlugin; import software.amazon.jdbc.plugin.AuroraHostListConnectionPlugin; +import software.amazon.jdbc.plugin.AuroraInitialConnectionStrategyPlugin; import software.amazon.jdbc.plugin.AwsSecretsManagerConnectionPlugin; import software.amazon.jdbc.plugin.DataCacheConnectionPlugin; import software.amazon.jdbc.plugin.DefaultConnectionPlugin; @@ -43,6 +44,8 @@ import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin; import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin; import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsPlugin; +import software.amazon.jdbc.plugin.strategy.fastestresponse.FastestResponseStrategyPlugin; +import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlMethodAnalyzer; import software.amazon.jdbc.util.WrapperUtils; @@ -57,6 +60,7 @@ *

THIS CLASS IS NOT MULTI-THREADING SAFE IT'S EXPECTED TO HAVE ONE INSTANCE OF THIS MANAGER PER * JDBC CONNECTION */ +@SuppressWarnings("deprecation") public class ConnectionPluginManager implements CanReleaseResources, Wrapper { protected static final Map, String> pluginNameByClass = @@ -68,12 +72,15 @@ public class ConnectionPluginManager implements CanReleaseResources, Wrapper { put(LogQueryConnectionPlugin.class, "plugin:logQuery"); put(DataCacheConnectionPlugin.class, "plugin:dataCache"); put(HostMonitoringConnectionPlugin.class, "plugin:efm"); + put(software.amazon.jdbc.plugin.efm2.HostMonitoringConnectionPlugin.class, "plugin:efm2"); put(FailoverConnectionPlugin.class, "plugin:failover"); put(IamAuthConnectionPlugin.class, "plugin:iam"); put(AwsSecretsManagerConnectionPlugin.class, "plugin:awsSecretsManager"); put(AuroraStaleDnsPlugin.class, "plugin:auroraStaleDns"); put(ReadWriteSplittingPlugin.class, "plugin:readWriteSplitting"); + put(FastestResponseStrategyPlugin.class, "plugin:fastestResponseStrategy"); put(DefaultConnectionPlugin.class, "plugin:targetDriver"); + put(AuroraInitialConnectionStrategyPlugin.class, "plugin:initialConnection"); } }; @@ -91,7 +98,8 @@ public class ConnectionPluginManager implements CanReleaseResources, Wrapper { protected Properties props = new Properties(); protected List plugins; - protected final ConnectionProvider defaultConnProvider; + protected final @NonNull ConnectionProvider defaultConnProvider; + protected final @Nullable ConnectionProvider effectiveConnProvider; protected final ConnectionWrapper connectionWrapper; protected PluginService pluginService; protected TelemetryFactory telemetryFactory; @@ -99,10 +107,13 @@ public class ConnectionPluginManager implements CanReleaseResources, Wrapper { @SuppressWarnings("rawtypes") protected final Map pluginChainFuncMap = new HashMap<>(); - public ConnectionPluginManager(final ConnectionProvider defaultConnProvider, - final ConnectionWrapper connectionWrapper, - final TelemetryFactory telemetryFactory) { + public ConnectionPluginManager( + final @NonNull ConnectionProvider defaultConnProvider, + final @Nullable ConnectionProvider effectiveConnProvider, + final @NonNull ConnectionWrapper connectionWrapper, + final @NonNull TelemetryFactory telemetryFactory) { this.defaultConnProvider = defaultConnProvider; + this.effectiveConnProvider = effectiveConnProvider; this.connectionWrapper = connectionWrapper; this.telemetryFactory = telemetryFactory; } @@ -111,13 +122,14 @@ public ConnectionPluginManager(final ConnectionProvider defaultConnProvider, * This constructor is for testing purposes only. */ ConnectionPluginManager( - final ConnectionProvider defaultConnProvider, + final @NonNull ConnectionProvider defaultConnProvider, + final @Nullable ConnectionProvider effectiveConnProvider, final Properties props, final ArrayList plugins, final ConnectionWrapper connectionWrapper, final PluginService pluginService, final TelemetryFactory telemetryFactory) { - this(defaultConnProvider, props, plugins, connectionWrapper, telemetryFactory); + this(defaultConnProvider, effectiveConnProvider, props, plugins, connectionWrapper, telemetryFactory); this.pluginService = pluginService; } @@ -125,12 +137,14 @@ public ConnectionPluginManager(final ConnectionProvider defaultConnProvider, * This constructor is for testing purposes only. */ ConnectionPluginManager( - final ConnectionProvider defaultConnProvider, + final @NonNull ConnectionProvider defaultConnProvider, + final @Nullable ConnectionProvider effectiveConnProvider, final Properties props, final ArrayList plugins, final ConnectionWrapper connectionWrapper, final TelemetryFactory telemetryFactory) { this.defaultConnProvider = defaultConnProvider; + this.effectiveConnProvider = effectiveConnProvider; this.props = props; this.plugins = plugins; this.connectionWrapper = connectionWrapper; @@ -156,10 +170,14 @@ public void unlock() { * @param pluginService a reference to a plugin service that plugin can use * @param props the configuration of the connection * @param pluginManagerService a reference to a plugin manager service + * @param configurationProfile a profile configuration defined by the user * @throws SQLException if errors occurred during the execution */ public void init( - final PluginService pluginService, final Properties props, final PluginManagerService pluginManagerService) + final PluginService pluginService, + final Properties props, + final PluginManagerService pluginManagerService, + @Nullable ConfigurationProfile configurationProfile) throws SQLException { this.props = props; @@ -170,8 +188,10 @@ public void init( this.plugins = pluginChainBuilder.getPlugins( this.pluginService, this.defaultConnProvider, + this.effectiveConnProvider, pluginManagerService, - props); + props, + configurationProfile); } protected T executeWithSubscribedPlugins( diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionProvider.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionProvider.java index f0333ea6b..337bebc82 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionProvider.java @@ -23,6 +23,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; /** * Implement this interface in order to handle the physical connection creation process. @@ -74,6 +75,7 @@ HostSpec getHostSpecByStrategy( * * @param protocol the connection protocol (example "jdbc:mysql://") * @param dialect the database dialect + * @param targetDriverDialect the target driver dialect * @param hostSpec the HostSpec containing the host-port information for the host to connect to * @param props the Properties to use for the connection * @return {@link Connection} resulting from the given connection information @@ -82,13 +84,15 @@ HostSpec getHostSpecByStrategy( Connection connect( @NonNull String protocol, @NonNull Dialect dialect, + @NonNull TargetDriverDialect targetDriverDialect, @NonNull HostSpec hostSpec, @NonNull Properties props) throws SQLException; /** * Called once per connection that needs to be created. - * This method is deprecated. Use {@link #connect(String, Dialect, HostSpec, Properties)} instead. + * This method is deprecated. + * Use {@link #connect(String, Dialect, TargetDriverDialect, HostSpec, Properties)} instead. * * @param url the connection URL * @param props the Properties to use for the connection diff --git a/wrapper/src/main/java/software/amazon/jdbc/DataSourceConnectionProvider.java b/wrapper/src/main/java/software/amazon/jdbc/DataSourceConnectionProvider.java index b56851e77..e1f278b2c 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/DataSourceConnectionProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/DataSourceConnectionProvider.java @@ -53,16 +53,11 @@ public class DataSourceConnectionProvider implements ConnectionProvider { }); private final @NonNull DataSource dataSource; private final @NonNull String dataSourceClassName; - private final @NonNull TargetDriverDialect targetDriverDialect; - private final ReentrantLock lock = new ReentrantLock(); - public DataSourceConnectionProvider( - final @NonNull DataSource dataSource, - final @NonNull TargetDriverDialect targetDriverDialect) { + public DataSourceConnectionProvider(final @NonNull DataSource dataSource) { this.dataSource = dataSource; - this.targetDriverDialect = targetDriverDialect; this.dataSourceClassName = dataSource.getClass().getName(); } @@ -115,6 +110,7 @@ public HostSpec getHostSpecByStrategy( public Connection connect( final @NonNull String protocol, final @NonNull Dialect dialect, + final @NonNull TargetDriverDialect targetDriverDialect, final @NonNull HostSpec hostSpec, final @NonNull Properties props) throws SQLException { @@ -129,7 +125,7 @@ public Connection connect( LOGGER.finest(() -> "Use a separate DataSource object to create a connection."); // use a new data source instance to instantiate a connection final DataSource ds = createDataSource(); - this.targetDriverDialect.prepareDataSource( + targetDriverDialect.prepareDataSource( ds, protocol, hostSpec, @@ -143,7 +139,7 @@ public Connection connect( this.lock.lock(); LOGGER.finest(() -> "Use main DataSource object to create a connection."); try { - this.targetDriverDialect.prepareDataSource( + targetDriverDialect.prepareDataSource( this.dataSource, protocol, hostSpec, diff --git a/wrapper/src/main/java/software/amazon/jdbc/Driver.java b/wrapper/src/main/java/software/amazon/jdbc/Driver.java index b077829fb..74c172969 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/Driver.java +++ b/wrapper/src/main/java/software/amazon/jdbc/Driver.java @@ -29,13 +29,18 @@ import java.util.logging.Handler; 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.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.DriverConfigurationProfiles; +import software.amazon.jdbc.states.ResetSessionStateOnCloseCallable; +import software.amazon.jdbc.states.TransferSessionStateOnSwitchCallable; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialectManager; import software.amazon.jdbc.util.ConnectionUrlParser; import software.amazon.jdbc.util.DriverInfo; import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.PropertyUtils; import software.amazon.jdbc.util.StringUtils; import software.amazon.jdbc.util.telemetry.DefaultTelemetryFactory; import software.amazon.jdbc.util.telemetry.TelemetryContext; @@ -50,6 +55,9 @@ public class Driver implements java.sql.Driver { private static final Logger LOGGER = Logger.getLogger("software.amazon.jdbc.Driver"); private static @Nullable Driver registeredDriver; + private static ResetSessionStateOnCloseCallable resetSessionStateOnCloseCallable = null; + private static TransferSessionStateOnSwitchCallable transferSessionStateOnSwitchCallable = null; + static { try { register(); @@ -100,41 +108,41 @@ public Connection connect(final String url, final Properties info) throws SQLExc LOGGER.finest("Opening connection to " + url); + ConnectionUrlParser.parsePropertiesFromUrl(url, info); + final Properties props = PropertyUtils.copyProperties(info); + final String databaseName = ConnectionUrlParser.parseDatabaseFromUrl(url); if (!StringUtils.isNullOrEmpty(databaseName)) { - PropertyDefinition.DATABASE.set(info, databaseName); + PropertyDefinition.DATABASE.set(props, databaseName); } - ConnectionUrlParser.parsePropertiesFromUrl(url, info); - TelemetryFactory telemetryFactory = new DefaultTelemetryFactory(info); + LOGGER.finest(() -> PropertyUtils.logProperties(props, "Connecting with properties: \n")); + + final String profileName = PropertyDefinition.PROFILE_NAME.getString(props); + ConfigurationProfile configurationProfile = null; + if (!StringUtils.isNullOrEmpty(profileName)) { + configurationProfile = DriverConfigurationProfiles.getProfileConfiguration(profileName); + if (configurationProfile != null) { + PropertyUtils.addProperties(props, configurationProfile.getProperties()); + } else { + throw new SQLException( + Messages.get( + "Driver.configurationProfileNotFound", + new Object[] {profileName})); + } + } + + TelemetryFactory telemetryFactory = new DefaultTelemetryFactory(props); TelemetryContext context = telemetryFactory.openTelemetryContext( "software.amazon.jdbc.Driver.connect", TelemetryTraceLevel.TOP_LEVEL); try { final String driverUrl = url.replaceFirst(PROTOCOL_PREFIX, "jdbc:"); - java.sql.Driver driver; - try { - driver = DriverManager.getDriver(driverUrl); - } catch (SQLException e) { - final List registeredDrivers = Collections.list(DriverManager.getDrivers()) - .stream() - .map(x -> x.getClass().getName()) - .collect(Collectors.toList()); - throw new SQLException( - Messages.get("Driver.missingDriver", new Object[] {driverUrl, registeredDrivers}), e); - } + TargetDriverHelper helper = new TargetDriverHelper(); + java.sql.Driver driver = helper.getTargetDriver(driverUrl, props); - if (driver == null) { - final List registeredDrivers = Collections.list(DriverManager.getDrivers()) - .stream() - .map(x -> x.getClass().getName()) - .collect(Collectors.toList()); - LOGGER.severe(() -> Messages.get("Driver.missingDriver", new Object[] {driverUrl, registeredDrivers})); - return null; - } - - final String logLevelStr = PropertyDefinition.LOGGER_LEVEL.getString(info); + final String logLevelStr = PropertyDefinition.LOGGER_LEVEL.getString(props); if (!StringUtils.isNullOrEmpty(logLevelStr)) { final Level logLevel = Level.parse(logLevelStr.toUpperCase()); final Logger rootLogger = Logger.getLogger(""); @@ -149,12 +157,30 @@ public Connection connect(final String url, final Properties info) throws SQLExc PARENT_LOGGER.setLevel(logLevel); } - final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); - final TargetDriverDialect targetDriverDialect = targetDriverDialectManager.getDialect(driver, info); + TargetDriverDialect targetDriverDialect = configurationProfile == null + ? null + : configurationProfile.getTargetDriverDialect(); - final ConnectionProvider connectionProvider = new DriverConnectionProvider(driver, targetDriverDialect); + if (targetDriverDialect == null) { + final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); + targetDriverDialect = targetDriverDialectManager.getDialect(driver, props); + } - return new ConnectionWrapper(info, driverUrl, connectionProvider, telemetryFactory); + final ConnectionProvider defaultConnectionProvider = new DriverConnectionProvider(driver); + + ConnectionProvider effectiveConnectionProvider = null; + if (configurationProfile != null) { + effectiveConnectionProvider = configurationProfile.getConnectionProvider(); + } + + return new ConnectionWrapper( + props, + driverUrl, + defaultConnectionProvider, + effectiveConnectionProvider, + targetDriverDialect, + configurationProfile, + telemetryFactory); } catch (Exception ex) { context.setException(ex); @@ -212,4 +238,28 @@ public boolean jdbcCompliant() { public Logger getParentLogger() throws SQLFeatureNotSupportedException { return PARENT_LOGGER; } + + public static void setResetSessionStateOnCloseFunc(final @NonNull ResetSessionStateOnCloseCallable func) { + resetSessionStateOnCloseCallable = func; + } + + public static void resetResetSessionStateOnCloseFunc() { + resetSessionStateOnCloseCallable = null; + } + + public static ResetSessionStateOnCloseCallable getResetSessionStateOnCloseFunc() { + return resetSessionStateOnCloseCallable; + } + + public static void setTransferSessionStateOnSwitchFunc(final @NonNull TransferSessionStateOnSwitchCallable func) { + transferSessionStateOnSwitchCallable = func; + } + + public static void resetTransferSessionStateOnSwitchFunc() { + transferSessionStateOnSwitchCallable = null; + } + + public static TransferSessionStateOnSwitchCallable getTransferSessionStateOnSwitchFunc() { + return transferSessionStateOnSwitchCallable; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/DriverConnectionProvider.java b/wrapper/src/main/java/software/amazon/jdbc/DriverConnectionProvider.java index c7a832f22..063fe1cf5 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/DriverConnectionProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/DriverConnectionProvider.java @@ -50,14 +50,10 @@ public class DriverConnectionProvider implements ConnectionProvider { }); private final java.sql.Driver driver; - private final @NonNull TargetDriverDialect targetDriverDialect; private final @NonNull String targetDriverClassName; - public DriverConnectionProvider( - final java.sql.Driver driver, - final @NonNull TargetDriverDialect targetDriverDialect) { + public DriverConnectionProvider(final java.sql.Driver driver) { this.driver = driver; - this.targetDriverDialect = targetDriverDialect; this.targetDriverClassName = driver.getClass().getName(); } @@ -101,6 +97,8 @@ public HostSpec getHostSpecByStrategy( * Called once per connection that needs to be created. * * @param protocol The connection protocol (example "jdbc:mysql://") + * @param dialect The database dialect + * @param targetDriverDialect The target driver dialect * @param hostSpec The HostSpec containing the host-port information for the host to connect to * @param props The Properties to use for the connection * @return {@link Connection} resulting from the given connection information @@ -110,13 +108,16 @@ public HostSpec getHostSpecByStrategy( public Connection connect( final @NonNull String protocol, final @NonNull Dialect dialect, + final @NonNull TargetDriverDialect targetDriverDialect, final @NonNull HostSpec hostSpec, final @NonNull Properties props) throws SQLException { + LOGGER.finest(() -> PropertyUtils.logProperties(props, "Connecting with properties: \n")); + final Properties copy = PropertyUtils.copyProperties(props); dialect.prepareConnectProperties(copy, protocol, hostSpec); - final ConnectInfo connectInfo = this.targetDriverDialect.prepareConnectInfo(protocol, hostSpec, copy); + final ConnectInfo connectInfo = targetDriverDialect.prepareConnectInfo(protocol, hostSpec, copy); LOGGER.finest(() -> "Connecting to " + connectInfo.url + PropertyUtils.logProperties(PropertyUtils.maskProperties(connectInfo.props), "\nwith properties: \n")); diff --git a/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java b/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java index bb9b1b857..4836d5bf1 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java @@ -21,9 +21,11 @@ import java.sql.Connection; import java.sql.SQLException; 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.Properties; import java.util.Set; import java.util.StringJoiner; @@ -34,13 +36,14 @@ import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.cleanup.CanReleaseResources; import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.targetdriverdialect.ConnectInfo; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.HikariCPSQLException; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; import software.amazon.jdbc.util.SlidingExpirationCache; -import software.amazon.jdbc.util.StringUtils; public class HikariPooledConnectionProvider implements PooledConnectionProvider, CanReleaseResources { @@ -173,7 +176,6 @@ public HostSpec getHostSpecByStrategy( "ConnectionProvider.unsupportedHostSpecSelectorStrategy", new Object[] {strategy, DataSourceConnectionProvider.class})); } - if (LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS.equals(strategy)) { return this.leastConnectionsHostSelector.getHost(hosts, role, props); } else { @@ -185,6 +187,7 @@ public HostSpec getHostSpecByStrategy( public Connection connect( @NonNull String protocol, @NonNull Dialect dialect, + @NonNull TargetDriverDialect targetDriverDialect, @NonNull HostSpec hostSpec, @NonNull Properties props) throws SQLException { @@ -194,7 +197,7 @@ public Connection connect( final HikariDataSource ds = databasePools.computeIfAbsent( new PoolKey(hostSpec.getUrl(), getPoolKey(hostSpec, copy)), - (lambdaPoolKey) -> createHikariDataSource(protocol, hostSpec, copy), + (lambdaPoolKey) -> createHikariDataSource(protocol, hostSpec, copy, targetDriverDialect), poolExpirationCheckNanos ); @@ -243,29 +246,44 @@ public void releaseResources() { * @param protocol the driver protocol that should be used to form connections * @param hostSpec the host details used to form the connection * @param connectionProps the connection properties + * @param targetDriverDialect the target driver dialect {@link TargetDriverDialect} */ protected void configurePool( - HikariConfig config, String protocol, HostSpec hostSpec, Properties connectionProps) { - StringBuilder urlBuilder = new StringBuilder().append(protocol).append(hostSpec.getUrl()); - - final String db = PropertyDefinition.DATABASE.getString(connectionProps); - if (!StringUtils.isNullOrEmpty(db)) { - urlBuilder.append(db); + final HikariConfig config, + final String protocol, + final HostSpec hostSpec, + final Properties connectionProps, + final @NonNull TargetDriverDialect targetDriverDialect) { + + final Properties copy = PropertyUtils.copyProperties(connectionProps); + + ConnectInfo connectInfo; + try { + connectInfo = targetDriverDialect.prepareConnectInfo( + protocol, hostSpec, copy); + } catch (SQLException ex) { + throw new RuntimeException(ex); } + StringBuilder urlBuilder = new StringBuilder(connectInfo.url); + final StringJoiner propsJoiner = new StringJoiner("&"); - connectionProps.forEach((k, v) -> { + connectInfo.props.forEach((k, v) -> { if (!PropertyDefinition.PASSWORD.name.equals(k) && !PropertyDefinition.USER.name.equals(k)) { propsJoiner.add(k + "=" + v); } }); - urlBuilder.append("?").append(propsJoiner); + + if (connectInfo.url.contains("?")) { + urlBuilder.append("&").append(propsJoiner); + } else { + urlBuilder.append("?").append(propsJoiner); + } config.setJdbcUrl(urlBuilder.toString()); - config.setExceptionOverrideClassName(HikariCPSQLException.class.getName()); - final String user = connectionProps.getProperty(PropertyDefinition.USER.name); - final String password = connectionProps.getProperty(PropertyDefinition.PASSWORD.name); + final String user = connectInfo.props.getProperty(PropertyDefinition.USER.name); + final String password = connectInfo.props.getProperty(PropertyDefinition.PASSWORD.name); if (user != null) { config.setUsername(user); } @@ -327,9 +345,14 @@ public void logConnections() { }); } - HikariDataSource createHikariDataSource(String protocol, HostSpec hostSpec, Properties props) { + HikariDataSource createHikariDataSource( + final String protocol, + final HostSpec hostSpec, + final Properties props, + final @NonNull TargetDriverDialect targetDriverDialect) { + HikariConfig config = poolConfigurator.configurePool(hostSpec, props); - configurePool(config, protocol, hostSpec, props); + configurePool(config, protocol, hostSpec, props, targetDriverDialect); return new HikariDataSource(config); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/HostSpec.java b/wrapper/src/main/java/software/amazon/jdbc/HostSpec.java index 4fcb563ea..736daa85f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HostSpec.java +++ b/wrapper/src/main/java/software/amazon/jdbc/HostSpec.java @@ -46,15 +46,31 @@ public class HostSpec { protected Timestamp lastUpdateTime; protected HostAvailabilityStrategy hostAvailabilityStrategy; - private HostSpec(final String host, final int port, final HostRole role, final HostAvailability availability, + private HostSpec( + final String host, + final int port, + final String hostId, + final HostRole role, + final HostAvailability availability, final HostAvailabilityStrategy hostAvailabilityStrategy) { - this(host, port, role, availability, DEFAULT_WEIGHT, Timestamp.from(Instant.now()), hostAvailabilityStrategy); + + this(host, port, hostId, role, availability, DEFAULT_WEIGHT, + Timestamp.from(Instant.now()), hostAvailabilityStrategy); } - HostSpec(final String host, final int port, final HostRole role, final HostAvailability availability, - final long weight, final Timestamp lastUpdateTime, final HostAvailabilityStrategy hostAvailabilityStrategy) { + HostSpec( + final String host, + final int port, + final String hostId, + final HostRole role, + final HostAvailability availability, + final long weight, + final Timestamp lastUpdateTime, + final HostAvailabilityStrategy hostAvailabilityStrategy) { + this.host = host; this.port = port; + this.hostId = hostId; this.availability = availability; this.role = role; this.allAliases.add(this.asAlias()); @@ -70,7 +86,7 @@ private HostSpec(final String host, final int port, final HostRole role, final H * @param role the role of this host (writer or reader). */ public HostSpec(final HostSpec copyHost, final HostRole role) { - this(copyHost.getHost(), copyHost.getPort(), role, copyHost.getAvailability(), + this(copyHost.getHost(), copyHost.getPort(), copyHost.getHostId(), role, copyHost.getAvailability(), copyHost.getHostAvailabilityStrategy()); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/HostSpecBuilder.java b/wrapper/src/main/java/software/amazon/jdbc/HostSpecBuilder.java index e6c1313ca..156989b8a 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HostSpecBuilder.java +++ b/wrapper/src/main/java/software/amazon/jdbc/HostSpecBuilder.java @@ -21,10 +21,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.HostAvailabilityStrategy; -import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; public class HostSpecBuilder { private String host; + private String hostId; private int port = HostSpec.NO_PORT; private HostAvailability availability = HostAvailability.AVAILABLE; private HostRole role = HostRole.WRITER; @@ -39,6 +39,7 @@ public HostSpecBuilder(final @NonNull HostAvailabilityStrategy hostAvailabilityS public HostSpecBuilder(HostSpecBuilder hostSpecBuilder) { this.host = hostSpecBuilder.host; this.port = hostSpecBuilder.port; + this.hostId = hostSpecBuilder.hostId; this.availability = hostSpecBuilder.availability; this.role = hostSpecBuilder.role; this.weight = hostSpecBuilder.weight; @@ -56,6 +57,11 @@ public HostSpecBuilder port(int port) { return this; } + public HostSpecBuilder hostId(String hostId) { + this.hostId = hostId; + return this; + } + public HostSpecBuilder availability(HostAvailability availability) { this.availability = availability; return this; @@ -84,8 +90,8 @@ public HostSpecBuilder lastUpdateTime(Timestamp lastUpdateTime) { public HostSpec build() { checkHostIsSet(); setDefaultLastUpdateTime(); - return new HostSpec(this.host, this.port, this.role, this.availability, this.weight, this.lastUpdateTime, - this.hostAvailabilityStrategy); + return new HostSpec(this.host, this.port, this.hostId, this.role, this.availability, + this.weight, this.lastUpdateTime, this.hostAvailabilityStrategy); } private void checkHostIsSet() { diff --git a/wrapper/src/main/java/software/amazon/jdbc/PluginManagerService.java b/wrapper/src/main/java/software/amazon/jdbc/PluginManagerService.java index d85426c4a..79c9168da 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PluginManagerService.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PluginManagerService.java @@ -18,7 +18,5 @@ public interface PluginManagerService { - void setReadOnly(boolean readOnly); - void setInTransaction(boolean inTransaction); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/PluginService.java b/wrapper/src/main/java/software/amazon/jdbc/PluginService.java index 7f6a22d00..9ee3682f4 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PluginService.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PluginService.java @@ -27,7 +27,8 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.exceptions.ExceptionHandler; import software.amazon.jdbc.hostavailability.HostAvailability; -import software.amazon.jdbc.states.SessionDirtyFlag; +import software.amazon.jdbc.states.SessionStateService; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.telemetry.TelemetryFactory; /** @@ -42,24 +43,25 @@ public interface PluginService extends ExceptionHandler { void setCurrentConnection(final @NonNull Connection connection, final @NonNull HostSpec hostSpec) throws SQLException; + /** + * Set a new internal connection. While setting a new connection, a notification may be sent to all plugins. + * See {@link ConnectionPlugin#notifyConnectionChanged(EnumSet)} for more details. A plugin mentioned + * in parameter skipNotificationForThisPlugin won't be receiving such notification. + * + * @param connection the new internal connection. + * @param hostSpec the host details for a new internal connection. + * @param skipNotificationForThisPlugin A reference to a plugin that doesn't need to receive notification + * about connection change. Usually, a plugin that initiates connection change + * doesn't need to receive such notification and uses a pointer to + * itself as a call parameter. + * @return a set of notification options about this connection switch. + */ EnumSet setCurrentConnection( final @NonNull Connection connection, final @NonNull HostSpec hostSpec, @Nullable ConnectionPlugin skipNotificationForThisPlugin) throws SQLException; - EnumSet getCurrentConnectionState(); - - void setCurrentConnectionState(SessionDirtyFlag flag); - - void resetCurrentConnectionState(SessionDirtyFlag flag); - - void resetCurrentConnectionStates(); - - boolean getAutoCommit(); - - void setAutoCommit(final boolean autoCommit); - List getHosts(); HostSpec getInitialConnectionHostSpec(); @@ -110,10 +112,6 @@ HostSpec getHostSpecByStrategy(HostRole role, String strategy) void setAvailability(Set hostAliases, HostAvailability availability); - boolean isExplicitReadOnly(); - - boolean isReadOnly(); - boolean isInTransaction(); HostListProvider getHostListProvider(); @@ -165,6 +163,8 @@ HostSpec getHostSpecByStrategy(HostRole role, String strategy) Dialect getDialect(); + TargetDriverDialect getTargetDriverDialect(); + void updateDialect(final @NonNull Connection connection) throws SQLException; HostSpec identifyConnection(final Connection connection) throws SQLException; @@ -183,4 +183,5 @@ HostSpec getHostSpecByStrategy(HostRole role, String strategy) String getTargetName(); + @NonNull SessionStateService getSessionStateService(); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java index 2cd701359..2a32b417b 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java @@ -30,6 +30,7 @@ import java.util.Properties; import java.util.Set; 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; @@ -39,11 +40,15 @@ import software.amazon.jdbc.dialect.DialectManager; import software.amazon.jdbc.dialect.DialectProvider; import software.amazon.jdbc.dialect.HostListProviderSupplier; +import software.amazon.jdbc.exceptions.ExceptionHandler; import software.amazon.jdbc.exceptions.ExceptionManager; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.HostAvailabilityStrategyFactory; import software.amazon.jdbc.hostlistprovider.StaticHostListProvider; -import software.amazon.jdbc.states.SessionDirtyFlag; +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.CacheMap; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.telemetry.TelemetryFactory; @@ -65,19 +70,51 @@ public class PluginServiceImpl implements PluginService, CanReleaseResources, protected HostSpec currentHostSpec; protected HostSpec initialConnectionHostSpec; private boolean isInTransaction; - private boolean explicitReadOnly; private final ExceptionManager exceptionManager; + protected final @Nullable ExceptionHandler exceptionHandler; protected final DialectProvider dialectProvider; protected Dialect dialect; - protected EnumSet currentConnectionSessionState = EnumSet.noneOf(SessionDirtyFlag.class); - protected boolean isAutoCommit = false; + protected TargetDriverDialect targetDriverDialect; + protected @Nullable final ConfigurationProfile configurationProfile; + + protected final SessionStateService sessionStateService; + + protected final ReentrantLock connectionSwitchLock = new ReentrantLock(); public PluginServiceImpl( @NonNull final ConnectionPluginManager pluginManager, @NonNull final Properties props, @NonNull final String originalUrl, - final String targetDriverProtocol) throws SQLException { - this(pluginManager, new ExceptionManager(), props, originalUrl, targetDriverProtocol, + @NonNull final String targetDriverProtocol, + @NonNull final TargetDriverDialect targetDriverDialect) + throws SQLException { + + this(pluginManager, + new ExceptionManager(), + props, + originalUrl, + targetDriverProtocol, + null, + targetDriverDialect, + null, + null); + } + + public PluginServiceImpl( + @NonNull final ConnectionPluginManager pluginManager, + @NonNull final Properties props, + @NonNull final String originalUrl, + @NonNull final String targetDriverProtocol, + @NonNull final TargetDriverDialect targetDriverDialect, + @Nullable final ConfigurationProfile configurationProfile) throws SQLException { + this(pluginManager, + new ExceptionManager(), + props, + originalUrl, + targetDriverProtocol, + null, + targetDriverDialect, + configurationProfile, null); } @@ -86,15 +123,31 @@ public PluginServiceImpl( @NonNull final ExceptionManager exceptionManager, @NonNull final Properties props, @NonNull final String originalUrl, - final String targetDriverProtocol, - @Nullable final DialectProvider dialectProvider) throws SQLException { + @NonNull final String targetDriverProtocol, + @Nullable final DialectProvider dialectProvider, + @NonNull final TargetDriverDialect targetDriverDialect, + @Nullable final ConfigurationProfile configurationProfile, + @Nullable final SessionStateService sessionStateService) throws SQLException { this.pluginManager = pluginManager; this.props = props; this.originalUrl = originalUrl; this.driverProtocol = targetDriverProtocol; + this.configurationProfile = configurationProfile; this.exceptionManager = exceptionManager; this.dialectProvider = dialectProvider != null ? dialectProvider : new DialectManager(this); - this.dialect = this.dialectProvider.getDialect(this.driverProtocol, this.originalUrl, this.props); + this.targetDriverDialect = targetDriverDialect; + + this.sessionStateService = sessionStateService != null + ? sessionStateService + : new SessionStateServiceImpl(this, this.props); + + this.exceptionHandler = this.configurationProfile != null && this.configurationProfile.getExceptionHandler() != null + ? this.configurationProfile.getExceptionHandler() + : null; + + this.dialect = this.configurationProfile != null && this.configurationProfile.getDialect() != null + ? this.configurationProfile.getDialect() + : this.dialectProvider.getDialect(this.driverProtocol, this.originalUrl, this.props); } @Override @@ -175,54 +228,83 @@ public void setCurrentConnection( } @Override - public synchronized EnumSet setCurrentConnection( + public EnumSet setCurrentConnection( final @NonNull Connection connection, final @NonNull HostSpec hostSpec, @Nullable final ConnectionPlugin skipNotificationForThisPlugin) throws SQLException { - if (this.currentConnection == null) { - // setting up an initial connection - - this.currentConnection = connection; - this.currentHostSpec = hostSpec; - - final EnumSet changes = EnumSet.of(NodeChangeOptions.INITIAL_CONNECTION); - this.pluginManager.notifyConnectionChanged(changes, skipNotificationForThisPlugin); + connectionSwitchLock.lock(); + try { - return changes; + if (this.currentConnection == null) { + // setting up an initial connection - } else { - // update an existing connection + this.currentConnection = connection; + this.currentHostSpec = hostSpec; + this.sessionStateService.reset(); - final EnumSet changes = compare(this.currentConnection, this.currentHostSpec, - connection, hostSpec); + final EnumSet changes = EnumSet.of(NodeChangeOptions.INITIAL_CONNECTION); + this.pluginManager.notifyConnectionChanged(changes, skipNotificationForThisPlugin); - if (!changes.isEmpty()) { + return changes; - final Connection oldConnection = this.currentConnection; + } else { + // update an existing connection - this.currentConnection = connection; - this.currentHostSpec = hostSpec; - this.setInTransaction(false); + final EnumSet changes = compare(this.currentConnection, this.currentHostSpec, + connection, hostSpec); - final EnumSet pluginOpinions = this.pluginManager.notifyConnectionChanged( - changes, skipNotificationForThisPlugin); + if (!changes.isEmpty()) { - final boolean shouldCloseConnection = - changes.contains(NodeChangeOptions.CONNECTION_OBJECT_CHANGED) - && !oldConnection.isClosed() - && !pluginOpinions.contains(OldConnectionSuggestedAction.PRESERVE); + final Connection oldConnection = this.currentConnection; + final boolean isInTransaction = this.isInTransaction; + this.sessionStateService.begin(); - if (shouldCloseConnection) { try { - oldConnection.close(); - } catch (final SQLException e) { - // Ignore any exception + this.currentConnection = connection; + this.currentHostSpec = hostSpec; + + this.sessionStateService.applyCurrentSessionState(connection); + this.setInTransaction(false); + + if (isInTransaction && PropertyDefinition.ROLLBACK_ON_SWITCH.getBoolean(this.props)) { + try { + oldConnection.rollback(); + } catch (final SQLException e) { + // Ignore any exception + } + } + + final EnumSet pluginOpinions = this.pluginManager.notifyConnectionChanged( + changes, skipNotificationForThisPlugin); + + final boolean shouldCloseConnection = + changes.contains(NodeChangeOptions.CONNECTION_OBJECT_CHANGED) + && !oldConnection.isClosed() + && !pluginOpinions.contains(OldConnectionSuggestedAction.PRESERVE); + + if (shouldCloseConnection) { + try { + this.sessionStateService.applyPristineSessionState(oldConnection); + } catch (final SQLException e) { + // Ignore any exception + } + + try { + oldConnection.close(); + } catch (final SQLException e) { + // Ignore any exception + } + } + } finally { + this.sessionStateService.complete(); } } + return changes; } - return changes; + } finally { + connectionSwitchLock.unlock(); } } @@ -321,26 +403,11 @@ public void setAvailability(final @NonNull Set hostAliases, final @NonNu } } - @Override - public boolean isExplicitReadOnly() { - return this.explicitReadOnly; - } - - @Override - public boolean isReadOnly() { - return isExplicitReadOnly() || (this.currentHostSpec != null && this.currentHostSpec.getRole() != HostRole.WRITER); - } - @Override public boolean isInTransaction() { return this.isInTransaction; } - @Override - public void setReadOnly(final boolean readOnly) { - this.explicitReadOnly = readOnly; - } - @Override public void setInTransaction(final boolean inTransaction) { this.isInTransaction = inTransaction; @@ -478,22 +545,33 @@ public void releaseResources() { @Override public boolean isNetworkException(final Throwable throwable) { + if (this.exceptionHandler != null) { + return this.exceptionHandler.isNetworkException(throwable); + } return this.exceptionManager.isNetworkException(this.dialect, throwable); } @Override public boolean isNetworkException(final String sqlState) { + if (this.exceptionHandler != null) { + return this.exceptionHandler.isNetworkException(sqlState); + } return this.exceptionManager.isNetworkException(this.dialect, sqlState); } @Override public boolean isLoginException(final Throwable throwable) { + if (this.exceptionHandler != null) { + return this.exceptionHandler.isLoginException(throwable); + } return this.exceptionManager.isLoginException(this.dialect, throwable); - } @Override public boolean isLoginException(final String sqlState) { + if (this.exceptionHandler != null) { + return this.exceptionHandler.isLoginException(sqlState); + } return this.exceptionManager.isLoginException(this.dialect, sqlState); } @@ -502,6 +580,11 @@ public Dialect getDialect() { return this.dialect; } + @Override + public TargetDriverDialect getTargetDriverDialect() { + return this.targetDriverDialect; + } + public void updateDialect(final @NonNull Connection connection) throws SQLException { final Dialect originalDialect = this.dialect; this.dialect = this.dialectProvider.getDialect( @@ -571,27 +654,8 @@ public String getTargetName() { return this.pluginManager.getDefaultConnProvider().getTargetName(); } - public EnumSet getCurrentConnectionState() { - return this.currentConnectionSessionState.clone(); - } - - public void setCurrentConnectionState(SessionDirtyFlag flag) { - this.currentConnectionSessionState.add(flag); - } - - public void resetCurrentConnectionState(SessionDirtyFlag flag) { - this.currentConnectionSessionState.remove(flag); - } - - public void resetCurrentConnectionStates() { - this.currentConnectionSessionState.clear(); - } - - public boolean getAutoCommit() { - return this.isAutoCommit; - } - - public void setAutoCommit(final boolean autoCommit) { - this.isAutoCommit = autoCommit; + @Override + public @NonNull SessionStateService getSessionStateService() { + return this.sessionStateService; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java b/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java index 24e223cb3..ebcbdae67 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java @@ -101,6 +101,57 @@ public class PropertyDefinition { "OTLP", "NONE" }); + public static final AwsWrapperProperty LOGIN_TIMEOUT = + new AwsWrapperProperty( + "loginTimeout", null, "Login timeout in msec."); + + public static final AwsWrapperProperty CONNECT_TIMEOUT = + new AwsWrapperProperty( + "connectTimeout", null, "Socket connect timeout in msec."); + public static final AwsWrapperProperty SOCKET_TIMEOUT = + new AwsWrapperProperty( + "socketTimeout", null, "Socket timeout in msec."); + + public static final AwsWrapperProperty TCP_KEEP_ALIVE = + new AwsWrapperProperty( + "tcpKeepAlive", + "false", + "Enable or disable TCP keep-alive probe.", + false, + new String[] { + "true", "false" + }); + + public static final AwsWrapperProperty TRANSFER_SESSION_STATE_ON_SWITCH = + new AwsWrapperProperty( + "transferSessionStateOnSwitch", + "true", + "Enables session state transfer to a new connection.", + false, + new String[] { + "true", "false" + }); + + public static final AwsWrapperProperty RESET_SESSION_STATE_ON_CLOSE = + new AwsWrapperProperty( + "resetSessionStateOnClose", + "true", + "Enables to reset connection session state before closing it.", + false, + new String[] { + "true", "false" + }); + + public static final AwsWrapperProperty ROLLBACK_ON_SWITCH = + new AwsWrapperProperty( + "rollbackOnSwitch", + "true", + "Enables to rollback a current transaction being in progress when switching to a new connection.", + false, + new String[] { + "true", "false" + }); + private static final Map PROPS_BY_NAME = new ConcurrentHashMap<>(); private static final Set KNOWN_PROPS_BY_PREFIX = ConcurrentHashMap.newKeySet(); diff --git a/wrapper/src/main/java/software/amazon/jdbc/RoundRobinHostSelector.java b/wrapper/src/main/java/software/amazon/jdbc/RoundRobinHostSelector.java index 5e284b546..cb489b770 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/RoundRobinHostSelector.java +++ b/wrapper/src/main/java/software/amazon/jdbc/RoundRobinHostSelector.java @@ -107,27 +107,26 @@ private void createCacheEntryForHosts( final @NonNull List hosts, final @Nullable Properties props) throws SQLException { - final List hostsMissingCacheEntry = new ArrayList<>(); final List hostsWithCacheEntry = new ArrayList<>(); for (final HostSpec host : hosts) { if (roundRobinCache.get(host.getHost()) != null) { hostsWithCacheEntry.add(host); - } else { - hostsMissingCacheEntry.add(host); } } - if ((hostsMissingCacheEntry.isEmpty() && !hostsWithCacheEntry.isEmpty())) { + // If there is a host with an existing entry, update the cache entries for all hosts to point each to the same + // RoundRobinClusterInfo object. If there are no cache entries, create a new RoundRobinClusterInfo. + if (!hostsWithCacheEntry.isEmpty()) { for (final HostSpec host : hosts) { roundRobinCache.put( host.getHost(), roundRobinCache.get(hostsWithCacheEntry.get(0).getHost()), DEFAULT_ROUND_ROBIN_CACHE_EXPIRE_NANO); } - } else if (hostsWithCacheEntry.isEmpty()) { + } else { final RoundRobinClusterInfo roundRobinClusterInfo = new RoundRobinClusterInfo(); updateCachePropertiesForRoundRobinClusterInfo(roundRobinClusterInfo, props); - for (final HostSpec host : hostsMissingCacheEntry) { + for (final HostSpec host : hosts) { roundRobinCache.put( host.getHost(), roundRobinClusterInfo, diff --git a/wrapper/src/main/java/software/amazon/jdbc/TargetDriverHelper.java b/wrapper/src/main/java/software/amazon/jdbc/TargetDriverHelper.java new file mode 100644 index 000000000..c7198f656 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/TargetDriverHelper.java @@ -0,0 +1,85 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package software.amazon.jdbc; + +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialectManager; +import software.amazon.jdbc.util.ConnectionUrlParser; +import software.amazon.jdbc.util.Messages; + +public class TargetDriverHelper { + + /** + * The method returns a driver for specified url. If driver couldn't be found, + * the method tries to identify a driver that corresponds to an url and register it. + * Registration of the driver could be disabled by provided configuration properties. + * If driver couldn't be found and couldn't be registered, the method raises an exception. + * + * @throws SQLException when a driver couldn't be found. + */ + public java.sql.Driver getTargetDriver( + final @NonNull String driverUrl, + final @NonNull Properties props) + throws SQLException { + + final ConnectionUrlParser parser = new ConnectionUrlParser(); + final String protocol = parser.getProtocol(driverUrl); + + TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); + java.sql.Driver targetDriver = null; + SQLException lastException = null; + + // Try to get a driver that can handle this url. + try { + targetDriver = DriverManager.getDriver(driverUrl); + } catch (SQLException e) { + lastException = e; + } + + // If the driver isn't found, it's possible to register a driver that corresponds to the protocol + // and try again. + if (targetDriver == null) { + boolean triedToRegister = targetDriverDialectManager.registerDriver(protocol, props); + if (triedToRegister) { + // There was an attempt to register a corresponding to the protocol driver. Try to find the driver again. + try { + targetDriver = DriverManager.getDriver(driverUrl); + } catch (SQLException e) { + lastException = e; + } + } + } + + // The driver is not found yet. Let's raise an exception. + if (targetDriver == null) { + final List registeredDrivers = Collections.list(DriverManager.getDrivers()) + .stream() + .map(x -> x.getClass().getName()) + .collect(Collectors.toList()); + throw new SQLException( + Messages.get("Driver.missingDriver", new Object[] {driverUrl, registeredDrivers}), lastException); + } + + return targetDriver; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java b/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java index cc1005467..dbac70b25 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java @@ -17,6 +17,7 @@ package software.amazon.jdbc.authentication; import java.util.Properties; +import java.util.concurrent.locks.ReentrantLock; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.jdbc.HostSpec; @@ -25,24 +26,40 @@ public class AwsCredentialsManager { private static AwsCredentialsProviderHandler handler = null; - public static synchronized void setCustomHandler(final AwsCredentialsProviderHandler customHandler) { - handler = customHandler; + private static final ReentrantLock lock = new ReentrantLock(); + + public static void setCustomHandler(final AwsCredentialsProviderHandler customHandler) { + lock.lock(); + try { + handler = customHandler; + } finally { + lock.unlock(); + } } - public static synchronized void resetCustomHandler() { - handler = null; + public static void resetCustomHandler() { + lock.lock(); + try { + handler = null; + } finally { + lock.unlock(); + } } - public static synchronized AwsCredentialsProvider getProvider( - final HostSpec hostSpec, - final Properties props) { - final AwsCredentialsProvider provider = handler != null - ? handler.getAwsCredentialsProvider(hostSpec, props) - : getDefaultProvider(); - if (provider == null) { - throw new IllegalArgumentException(Messages.get("AwsCredentialsManager.nullProvider")); + public static AwsCredentialsProvider getProvider(final HostSpec hostSpec, final Properties props) { + lock.lock(); + try { + final AwsCredentialsProvider provider = handler != null + ? handler.getAwsCredentialsProvider(hostSpec, props) + : getDefaultProvider(); + + if (provider == null) { + throw new IllegalArgumentException(Messages.get("AwsCredentialsManager.nullProvider")); + } + return provider; + } finally { + lock.unlock(); } - return provider; } private static AwsCredentialsProvider getDefaultProvider() { 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 de048f05e..fd9b85a07 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java @@ -38,14 +38,32 @@ public class AuroraMysqlDialect extends MysqlDialect { @Override public boolean isDialect(final Connection connection) { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'aurora_version'")) { + 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; } 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 d6a83ae35..098440a9f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java @@ -54,34 +54,67 @@ public boolean isDialect(final Connection connection) { return false; } + Statement stmt = null; + ResultSet rs = null; boolean hasExtensions = false; boolean hasTopology = false; try { - try (final Statement stmt = connection.createStatement(); - final ResultSet 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; - } + 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; } } - - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(topologySql)) { - if (rs.next()) { - LOGGER.finest(() -> "hasTopology: true"); - hasTopology = true; + } catch (SQLException ex) { + // ignore + } finally { + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException ex) { + // ignore } } - - return hasExtensions && hasTopology; - + if (rs != null) { + try { + rs.close(); + } catch (SQLException ex) { + // ignore + } + } + } + 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 false; + return hasExtensions && hasTopology; } @Override 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 87ac672e9..eb1357e19 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java @@ -66,7 +66,12 @@ public class DialectManager implements DialectProvider { } }; - protected static final long ENDPOINT_CACHE_EXPIRATION = TimeUnit.MINUTES.toNanos(30); + /** + * In order to simplify dialect detection, there's an internal host-to-dialect cache. + * The cache contains host endpoints and identified dialect. Cache expiration time + * is defined by the variable below. + */ + protected static final long ENDPOINT_CACHE_EXPIRATION = TimeUnit.HOURS.toNanos(24); // Map of host name, or url, by dialect code. protected static final CacheMap knownEndpointDialects = new CacheMap<>(); 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 85586134a..63efa1848 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java @@ -57,8 +57,11 @@ public String getServerVersionQuery() { @Override public boolean isDialect(final Connection connection) { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(this.getServerVersionQuery())) { + Statement stmt = null; + ResultSet rs = null; + try { + stmt = connection.createStatement(); + rs = stmt.executeQuery(this.getServerVersionQuery()); while (rs.next()) { final String columnValue = rs.getString(1); if (columnValue != null && columnValue.toLowerCase().contains("mariadb")) { @@ -67,6 +70,21 @@ 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; } 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 cb0693d34..6ba7d0613 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java @@ -62,8 +62,11 @@ public String getServerVersionQuery() { @Override public boolean isDialect(final Connection connection) { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(this.getServerVersionQuery())) { + Statement stmt = null; + ResultSet rs = null; + try { + stmt = connection.createStatement(); + rs = stmt.executeQuery(this.getServerVersionQuery()); while (rs.next()) { final int columnCount = rs.getMetaData().getColumnCount(); for (int i = 1; i <= columnCount; i++) { @@ -75,6 +78,21 @@ 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; } 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 9d9ef65fe..fb3c03167 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java @@ -66,15 +66,31 @@ public String getServerVersionQuery() { @Override public boolean isDialect(final Connection connection) { + Statement stmt = null; + ResultSet rs = null; try { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery("SELECT 1 FROM pg_proc LIMIT 1")) { - if (rs.next()) { - return true; - } + stmt = connection.createStatement(); + rs = stmt.executeQuery("SELECT 1 FROM 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; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java index 49669161a..4d3e5ca05 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java @@ -31,6 +31,10 @@ public class RdsMultiAzDbClusterMysqlDialect extends MysqlDialect { private static final String TOPOLOGY_QUERY = "SELECT id, endpoint, port FROM mysql.rds_topology"; + private static final String TOPOLOGY_TABLE_EXIST_QUERY = + "SELECT 1 AS tmp FROM information_schema.tables WHERE" + + " table_schema = 'mysql' AND table_name = 'rds_topology'"; + private static final String FETCH_WRITER_NODE_QUERY = "SHOW REPLICA STATUS"; private static final String FETCH_WRITER_NODE_QUERY_COLUMN_NAME = "Source_Server_Id"; @@ -40,11 +44,39 @@ public class RdsMultiAzDbClusterMysqlDialect extends MysqlDialect { @Override public boolean isDialect(final Connection connection) { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(TOPOLOGY_QUERY)) { - return rs.next(); + Statement stmt = null; + ResultSet rs = null; + try { + stmt = connection.createStatement(); + rs = stmt.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY); + + if (rs.next()) { + rs.close(); + stmt.close(); + + stmt = connection.createStatement(); + rs = stmt.executeQuery(TOPOLOGY_QUERY); + + return rs.next(); + } + 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; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java index 0f0f2fb7a..215a07619 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java @@ -33,6 +33,10 @@ public class RdsMultiAzDbClusterPgDialect extends PgDialect { private static final String TOPOLOGY_QUERY = "SELECT id, endpoint, port FROM rds_tools.show_topology('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; + private static final String WRITER_NODE_FUNC_EXIST_QUERY = + "SELECT 1 AS tmp FROM information_schema.routines" + + " WHERE routine_schema='rds_tools' AND routine_name='multi_az_db_cluster_source_dbi_resource_id'"; + 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()"; @@ -51,11 +55,39 @@ public ExceptionHandler getExceptionHandler() { @Override public boolean isDialect(final Connection connection) { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(FETCH_WRITER_NODE_QUERY)) { - return rs.next(); + Statement stmt = null; + ResultSet rs = null; + try { + stmt = connection.createStatement(); + rs = stmt.executeQuery(WRITER_NODE_FUNC_EXIST_QUERY); + + if (rs.next()) { + rs.close(); + stmt.close(); + + stmt = connection.createStatement(); + rs = stmt.executeQuery(FETCH_WRITER_NODE_QUERY); + + return rs.next(); + } + return false; } 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; } 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 b5d04cbef..a012d867f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java @@ -33,8 +33,12 @@ public boolean isDialect(final Connection connection) { if (!super.isDialect(connection)) { return false; } - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(this.getServerVersionQuery())) { + Statement stmt = null; + ResultSet rs = null; + + try { + stmt = connection.createStatement(); + rs = stmt.executeQuery(this.getServerVersionQuery()); while (rs.next()) { final int columnCount = rs.getMetaData().getColumnCount(); for (int i = 1; i <= columnCount; i++) { @@ -46,6 +50,21 @@ 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; } 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 c90c6ff0e..127b73b66 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java @@ -47,21 +47,37 @@ public boolean isDialect(final Connection connection) { if (!super.isDialect(connection)) { return false; } + Statement stmt = null; + ResultSet rs = null; try { - try (final Statement stmt = connection.createStatement(); - final ResultSet rs = stmt.executeQuery(extensionsSql)) { - 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)); - if (rdsTools && !auroraUtils) { - return true; - } + stmt = connection.createStatement(); + rs = stmt.executeQuery(extensionsSql); + 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)); + 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; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/ds/AwsWrapperDataSource.java b/wrapper/src/main/java/software/amazon/jdbc/ds/AwsWrapperDataSource.java index e8b47ed8f..e6a33a728 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ds/AwsWrapperDataSource.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ds/AwsWrapperDataSource.java @@ -25,9 +25,12 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; @@ -41,6 +44,9 @@ import software.amazon.jdbc.DriverConnectionProvider; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.TargetDriverHelper; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.DriverConfigurationProfiles; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialectManager; import software.amazon.jdbc.util.ConnectionUrlParser; @@ -59,6 +65,8 @@ public class AwsWrapperDataSource implements DataSource, Referenceable, Serializ private static final Logger LOGGER = Logger.getLogger(AwsWrapperDataSource.class.getName()); + private static final String PROTOCOL_PREFIX = "jdbc:aws-wrapper:"; + private static final String SERVER_NAME = "serverName"; private static final String SERVER_PORT = "serverPort"; @@ -96,6 +104,21 @@ public Connection getConnection(final String username, final String password) th this.password = password; final Properties props = PropertyUtils.copyProperties(this.targetDataSourceProperties); + + final String profileName = PropertyDefinition.PROFILE_NAME.getString(props); + ConfigurationProfile configurationProfile = null; + if (!StringUtils.isNullOrEmpty(profileName)) { + configurationProfile = DriverConfigurationProfiles.getProfileConfiguration(profileName); + if (configurationProfile != null) { + PropertyUtils.addProperties(props, configurationProfile.getProperties()); + } else { + throw new SQLException( + Messages.get( + "AwsWrapperDataSource.configurationProfileNotFound", + new Object[] {profileName})); + } + } + String finalUrl; final TelemetryFactory telemetryFactory = new DefaultTelemetryFactory(props); @@ -106,7 +129,8 @@ public Connection getConnection(final String username, final String password) th try { // Identify the URL for connection. if (!StringUtils.isNullOrEmpty(this.jdbcUrl)) { - finalUrl = this.jdbcUrl; + finalUrl = this.jdbcUrl.replaceFirst(PROTOCOL_PREFIX, "jdbc:"); + parsePropertiesFromUrl(this.jdbcUrl, props); setDatabasePropertyFromUrl(props); @@ -152,6 +176,15 @@ public Connection getConnection(final String username, final String password) th } } + TargetDriverDialect targetDriverDialect = configurationProfile == null + ? null + : configurationProfile.getTargetDriverDialect(); + + ConnectionProvider effectiveConnectionProvider = null; + if (configurationProfile != null) { + effectiveConnectionProvider = configurationProfile.getConnectionProvider(); + } + // Identify what connection provider to use. if (!StringUtils.isNullOrEmpty(this.targetDataSourceClassName)) { @@ -167,32 +200,39 @@ public Connection getConnection(final String username, final String password) th new Object[] {"loginTimeout", targetDataSource.getClass(), ex.getCause().getMessage()})); } - final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); - final TargetDriverDialect targetDriverDialect = - targetDriverDialectManager.getDialect(this.targetDataSourceClassName, props); + if (targetDriverDialect == null) { + final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); + targetDriverDialect = targetDriverDialectManager.getDialect(this.targetDataSourceClassName, props); + } + + ConnectionProvider defaultConnectionProvider = new DataSourceConnectionProvider(targetDataSource); return createConnectionWrapper( props, finalUrl, - new DataSourceConnectionProvider(targetDataSource, targetDriverDialect), + defaultConnectionProvider, + effectiveConnectionProvider, + targetDriverDialect, + configurationProfile, telemetryFactory); } else { + TargetDriverHelper helper = new TargetDriverHelper(); + final java.sql.Driver targetDriver = helper.getTargetDriver(finalUrl, props); - final java.sql.Driver targetDriver = DriverManager.getDriver(finalUrl); - - if (targetDriver == null) { - throw new SQLException(Messages.get("AwsWrapperDataSource.missingDriver", - new Object[]{finalUrl})); + if (targetDriverDialect == null) { + final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); + targetDriverDialect = targetDriverDialectManager.getDialect(targetDriver, props); } - final TargetDriverDialectManager targetDriverDialectManager = new TargetDriverDialectManager(); - final TargetDriverDialect targetDriverDialect = - targetDriverDialectManager.getDialect(targetDriver, props); + ConnectionProvider defaultConnectionProvider = new DriverConnectionProvider(targetDriver); return createConnectionWrapper( props, finalUrl, - new DriverConnectionProvider(targetDriver, targetDriverDialect), + defaultConnectionProvider, + effectiveConnectionProvider, + targetDriverDialect, + configurationProfile, telemetryFactory); } } catch (Exception ex) { @@ -207,9 +247,19 @@ public Connection getConnection(final String username, final String password) th ConnectionWrapper createConnectionWrapper( final Properties props, final String url, - final ConnectionProvider provider, + final @NonNull ConnectionProvider defaultProvider, + final @Nullable ConnectionProvider effectiveProvider, + final @NonNull TargetDriverDialect targetDriverDialect, + final @Nullable ConfigurationProfile configurationProfile, final TelemetryFactory telemetryFactory) throws SQLException { - return new ConnectionWrapper(props, url, provider, telemetryFactory); + return new ConnectionWrapper( + props, + url, + defaultProvider, + effectiveProvider, + targetDriverDialect, + configurationProfile, + telemetryFactory); } public void setTargetDataSourceClassName(@Nullable final String dataSourceClassName) { @@ -260,11 +310,11 @@ public void setJdbcProtocol(@NonNull final String jdbcProtocol) { return this.jdbcProtocol; } - public void setTargetDataSourceProperties(final Properties dataSourceProps) { + public void setTargetDataSourceProperties(final @Nullable Properties dataSourceProps) { this.targetDataSourceProperties = dataSourceProps; } - public Properties getTargetDataSourceProperties() { + public @Nullable Properties getTargetDataSourceProperties() { return this.targetDataSourceProperties; } @@ -379,7 +429,7 @@ private void setCredentialPropertiesFromUrl(final String jdbcUrl) { this.user = ConnectionUrlParser.parseUserFromUrl(jdbcUrl); } - if (!StringUtils.isNullOrEmpty(this.password)) { + if (StringUtils.isNullOrEmpty(this.password)) { this.password = ConnectionUrlParser.parsePasswordFromUrl(jdbcUrl); } } 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 af6a4bdf6..cf58d064f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java @@ -113,7 +113,7 @@ public class RdsHostListProvider implements DynamicHostListProvider { // (rather than a GUID or a value provided by the user). protected boolean isPrimaryClusterId; - protected boolean isInitialized = false; + protected volatile boolean isInitialized = false; static final Logger LOGGER = Logger.getLogger(RdsHostListProvider.class.getName()); @@ -509,7 +509,7 @@ public List refresh(final Connection connection) throws SQLException { : this.hostListProviderService.getCurrentConnection(); final FetchTopologyResult results = getTopology(currentConnection, false); - LOGGER.finest(() -> Utils.logTopology(results.hosts)); + LOGGER.finest(() -> Utils.logTopology(results.hosts, results.isCachedData ? "[From cache] " : "")); this.hostList = results.hosts; return Collections.unmodifiableList(hostList); 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 61efd6344..b0e0e6a23 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java @@ -76,7 +76,7 @@ public boolean acceptsStrategy(HostRole role, String strategy) { @Override public HostSpec getHostSpecByStrategy(final HostRole role, final String strategy) - throws UnsupportedOperationException { + throws SQLException, UnsupportedOperationException { throw new UnsupportedOperationException("getHostSpecByStrategy is not supported by this plugin."); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPlugin.java index d92c02a02..abe50e062 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPlugin.java @@ -115,30 +115,48 @@ public T execute(final Class resultClass, final Clas final Object[] jdbcMethodArgs) throws E { final HostSpec currentHostSpec = this.pluginService.getCurrentHostSpec(); - if (this.currentWriter == null || this.needUpdateCurrentWriter) { - this.currentWriter = this.getWriter(this.pluginService.getHosts()); - this.needUpdateCurrentWriter = false; - } + this.rememberWriter(); try { final T result = jdbcMethodFunc.call(); if ((methodName.equals(METHOD_CLOSE) || methodName.equals(METHOD_ABORT))) { tracker.invalidateCurrentConnection(currentHostSpec, this.pluginService.getCurrentConnection()); + } else if (this.needUpdateCurrentWriter) { + this.checkWriterChanged(); } return result; } catch (final Exception e) { if (e instanceof FailoverSQLException) { - if (!Objects.equals(this.getWriter(this.pluginService.getHosts()), this.currentWriter)) { - tracker.invalidateAllConnections(this.currentWriter); - tracker.logOpenedConnections(); - this.needUpdateCurrentWriter = true; - } + this.checkWriterChanged(); } throw e; } } + private void checkWriterChanged() { + final HostSpec hostSpecAfterFailover = this.getWriter(this.pluginService.getHosts()); + + if (this.currentWriter == null) { + this.currentWriter = hostSpecAfterFailover; + this.needUpdateCurrentWriter = false; + + } else if (!this.currentWriter.equals(hostSpecAfterFailover)) { + // the writer's changed + tracker.invalidateAllConnections(this.currentWriter); + tracker.logOpenedConnections(); + this.currentWriter = hostSpecAfterFailover; + this.needUpdateCurrentWriter = false; + } + } + + private void rememberWriter() { + if (this.currentWriter == null || this.needUpdateCurrentWriter) { + this.currentWriter = this.getWriter(this.pluginService.getHosts()); + this.needUpdateCurrentWriter = false; + } + } + @Override public void notifyNodeListChanged(final Map> changes) { for (final String node : changes.keySet()) { diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraHostListConnectionPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraHostListConnectionPluginFactory.java index 4447cec41..fe7e33b8a 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraHostListConnectionPluginFactory.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraHostListConnectionPluginFactory.java @@ -21,6 +21,7 @@ import software.amazon.jdbc.ConnectionPluginFactory; import software.amazon.jdbc.PluginService; +@SuppressWarnings("deprecation") public class AuroraHostListConnectionPluginFactory implements ConnectionPluginFactory { @Override diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java new file mode 100644 index 000000000..15ad1cf0d --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java @@ -0,0 +1,405 @@ +/* + * 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.plugin; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +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.util.Messages; +import software.amazon.jdbc.util.RdsUrlType; +import software.amazon.jdbc.util.RdsUtils; +import software.amazon.jdbc.util.WrapperUtils; + +public class AuroraInitialConnectionStrategyPlugin extends AbstractConnectionPlugin { + + private static final Logger LOGGER = Logger.getLogger(AuroraInitialConnectionStrategyPlugin.class.getName()); + + private static final Set subscribedMethods = + Collections.unmodifiableSet(new HashSet() { + { + add("initHostProvider"); + add("connect"); + add("forceConnect"); + } + }); + + public static final AwsWrapperProperty READER_HOST_SELECTOR_STRATEGY = + new AwsWrapperProperty( + "readerInitialConnectionHostSelectorStrategy", + "random", + "The strategy that should be used to select a new reader host while opening a new connection."); + + public static final AwsWrapperProperty OPEN_CONNECTION_RETRY_TIMEOUT_MS = + new AwsWrapperProperty( + "openConnectionRetryTimeoutMs", + "30000", + "Maximum allowed time for the retries opening a connection."); + + public static final AwsWrapperProperty OPEN_CONNECTION_RETRY_INTERVAL_MS = + new AwsWrapperProperty( + "openConnectionRetryIntervalMs", + "1000", + "Time between each retry of opening a connection."); + + private final PluginService pluginService; + private HostListProviderService hostListProviderService; + private final RdsUtils rdsUtils = new RdsUtils(); + + static { + PropertyDefinition.registerPluginProperties(AuroraInitialConnectionStrategyPlugin.class); + } + + public AuroraInitialConnectionStrategyPlugin(final PluginService pluginService, final Properties properties) { + this.pluginService = pluginService; + } + + @Override + public Set getSubscribedMethods() { + return subscribedMethods; + } + + @Override + public void initHostProvider( + final String driverProtocol, + final String initialUrl, + final Properties props, + final HostListProviderService hostListProviderService, + final JdbcCallable initHostProviderFunc) throws SQLException { + + this.hostListProviderService = hostListProviderService; + if (hostListProviderService.isStaticHostListProvider()) { + throw new SQLException(Messages.get("AuroraInitialConnectionStrategyPlugin.requireDynamicProvider")); + } + initHostProviderFunc.call(); + } + + @Override + public Connection connect( + final String driverProtocol, + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + + return this.connectInternal(hostSpec, props, isInitialConnection, connectFunc); + } + + @Override + public Connection forceConnect( + final String driverProtocol, + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable forceConnectFunc) + throws SQLException { + + return this.connectInternal(hostSpec, props, isInitialConnection, forceConnectFunc); + } + + private Connection connectInternal( + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + + final RdsUrlType type = this.rdsUtils.identifyRdsType(hostSpec.getHost()); + + if (!type.isRdsCluster()) { + // It's not a cluster endpoint. Continue with a normal workflow. + return connectFunc.call(); + } + + if (type == RdsUrlType.RDS_WRITER_CLUSTER) { + Connection writerCandidateConn = this.getVerifiedWriterConnection(props, isInitialConnection, connectFunc); + if (writerCandidateConn == null) { + // Can't get writer connection. Continue with a normal workflow. + return connectFunc.call(); + } + return writerCandidateConn; + } + + if (type == RdsUrlType.RDS_READER_CLUSTER) { + Connection readerCandidateConn = this.getVerifiedReaderConnection(props, isInitialConnection, connectFunc); + if (readerCandidateConn == null) { + // Can't get a reader connection. Continue with a normal workflow. + LOGGER.finest("Continue with normal workflow."); + return connectFunc.call(); + } + return readerCandidateConn; + } + + // Continue with a normal workflow. + return connectFunc.call(); + } + + private Connection getVerifiedWriterConnection( + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + + final int retryDelayMs = OPEN_CONNECTION_RETRY_INTERVAL_MS.getInteger(props); + + final long endTimeNano = this.getTime() + + TimeUnit.MILLISECONDS.toNanos(OPEN_CONNECTION_RETRY_TIMEOUT_MS.getInteger(props)); + + Connection writerCandidateConn; + HostSpec writerCandidate; + + while (this.getTime() < endTimeNano) { + + writerCandidateConn = null; + writerCandidate = null; + + try { + writerCandidate = this.getWriter(); + + if (writerCandidate == null || this.rdsUtils.isRdsClusterDns(writerCandidate.getHost())) { + + // Writer is not found. It seems that topology is outdated. + writerCandidateConn = connectFunc.call(); + this.pluginService.forceRefreshHostList(writerCandidateConn); + writerCandidate = this.pluginService.identifyConnection(writerCandidateConn); + + if (writerCandidate.getRole() != HostRole.WRITER) { + // Shouldn't be here. But let's try again. + this.closeConnection(writerCandidateConn); + this.delay(retryDelayMs); + continue; + } + + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(writerCandidate); + } + return writerCandidateConn; + } + + writerCandidateConn = this.pluginService.connect(writerCandidate, props); + + if (this.pluginService.getHostRole(writerCandidateConn) != HostRole.WRITER) { + // If the new connection resolves to a reader instance, this means the topology is outdated. + // Force refresh to update the topology. + this.pluginService.forceRefreshHostList(writerCandidateConn); + this.closeConnection(writerCandidateConn); + this.delay(retryDelayMs); + continue; + } + + // Writer connection is valid and verified. + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(writerCandidate); + } + return writerCandidateConn; + + } catch (SQLException ex) { + this.closeConnection(writerCandidateConn); + if (this.pluginService.isLoginException(ex)) { + throw WrapperUtils.wrapExceptionIfNeeded(SQLException.class, ex); + } else { + if (writerCandidate != null) { + this.pluginService.setAvailability(writerCandidate.asAliases(), HostAvailability.NOT_AVAILABLE); + } + } + } catch (Throwable ex) { + this.closeConnection(writerCandidateConn); + throw ex; + } + } + + return null; + } + + private Connection getVerifiedReaderConnection( + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + + final int retryDelayMs = OPEN_CONNECTION_RETRY_INTERVAL_MS.getInteger(props); + + final long endTimeNano = this.getTime() + + TimeUnit.MILLISECONDS.toNanos(OPEN_CONNECTION_RETRY_TIMEOUT_MS.getInteger(props)); + + Connection readerCandidateConn; + HostSpec readerCandidate; + + while (this.getTime() < endTimeNano) { + + readerCandidateConn = null; + readerCandidate = null; + + try { + readerCandidate = this.getReader(props); + + if (readerCandidate == null || this.rdsUtils.isRdsClusterDns(readerCandidate.getHost())) { + + // Reader is not found. It seems that topology is outdated. + readerCandidateConn = connectFunc.call(); + this.pluginService.forceRefreshHostList(readerCandidateConn); + readerCandidate = this.pluginService.identifyConnection(readerCandidateConn); + + if (readerCandidate.getRole() != HostRole.READER) { + if (this.hasNoReaders()) { + // It seems that cluster has no readers. Simulate Aurora reader cluster endpoint logic + // and return the current (writer) connection. + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(readerCandidate); + } + return readerCandidateConn; + } + this.closeConnection(readerCandidateConn); + this.delay(retryDelayMs); + continue; + } + + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(readerCandidate); + } + return readerCandidateConn; + } + + readerCandidateConn = this.pluginService.connect(readerCandidate, props); + + if (this.pluginService.getHostRole(readerCandidateConn) != HostRole.READER) { + // If the new connection resolves to a writer instance, this means the topology is outdated. + // Force refresh to update the topology. + this.pluginService.forceRefreshHostList(readerCandidateConn); + + if (this.hasNoReaders()) { + // It seems that cluster has no readers. Simulate Aurora reader cluster endpoint logic + // and return the current (writer) connection. + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(readerCandidate); + } + return readerCandidateConn; + } + + this.closeConnection(readerCandidateConn); + this.delay(retryDelayMs); + continue; + } + + // Reader connection is valid and verified. + if (isInitialConnection) { + hostListProviderService.setInitialConnectionHostSpec(readerCandidate); + } + return readerCandidateConn; + + } catch (SQLException ex) { + this.closeConnection(readerCandidateConn); + if (this.pluginService.isLoginException(ex)) { + throw WrapperUtils.wrapExceptionIfNeeded(SQLException.class, ex); + } else { + if (readerCandidate != null) { + this.pluginService.setAvailability(readerCandidate.asAliases(), HostAvailability.NOT_AVAILABLE); + } + } + } catch (Throwable ex) { + this.closeConnection(readerCandidateConn); + throw ex; + } + } + + return null; + } + + private void closeConnection(final Connection connection) { + if (connection != null) { + try { + connection.close(); + } catch (final SQLException ex) { + // ignore + } + } + } + + private void delay(final long delayMs) { + try { + TimeUnit.MILLISECONDS.sleep(delayMs); + } catch (InterruptedException ex) { + // ignore + } + } + + private HostSpec getWriter() { + for (final HostSpec host : this.pluginService.getHosts()) { + if (host.getRole() == HostRole.WRITER) { + return host; + } + } + return null; + } + + private HostSpec getReader(final Properties props) throws SQLException { + + final String strategy = READER_HOST_SELECTOR_STRATEGY.getString(props); + if (this.pluginService.acceptsStrategy(HostRole.READER, strategy)) { + try { + return this.pluginService.getHostSpecByStrategy(HostRole.READER, strategy); + } catch (UnsupportedOperationException ex) { + throw ex; + } catch (SQLException ex) { + // host isn't found + return null; + } + } + + throw new UnsupportedOperationException( + Messages.get( + "AuroraInitialConnectionStrategyPlugin.unsupportedStrategy", + new Object[] {strategy})); + } + + private boolean hasNoReaders() { + if (this.pluginService.getHosts().isEmpty()) { + // Topology inconclusive/corrupted. + return false; + } + + for (HostSpec hostSpec : this.pluginService.getHosts()) { + if (hostSpec.getRole() == HostRole.WRITER) { + continue; + } + + // Found a reader node + return false; + } + + // Went through all hosts and found no reader. + return true; + } + + // Method implemented to simplify unit testing. + protected long getTime() { + return System.nanoTime(); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPluginFactory.java new file mode 100644 index 000000000..842837dbd --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPluginFactory.java @@ -0,0 +1,29 @@ +/* + * 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.plugin; + +import java.util.Properties; +import software.amazon.jdbc.ConnectionPlugin; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.PluginService; + +public class AuroraInitialConnectionStrategyPluginFactory implements ConnectionPluginFactory { + @Override + public ConnectionPlugin getInstance(final PluginService pluginService, final Properties props) { + return new AuroraInitialConnectionStrategyPlugin(pluginService, props); + } +} 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 837adba9e..ab23d9166 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java @@ -29,6 +29,8 @@ import java.util.Set; 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.ConnectionPlugin; import software.amazon.jdbc.ConnectionProvider; import software.amazon.jdbc.ConnectionProviderManager; @@ -59,6 +61,9 @@ public final class DefaultConnectionPlugin implements ConnectionPlugin { Collections.singletonList("*"))); private static final SqlMethodAnalyzer sqlMethodAnalyzer = new SqlMethodAnalyzer(); + private final @NonNull ConnectionProvider defaultConnProvider; + private final @Nullable ConnectionProvider effectiveConnProvider; + private final ConnectionProviderManager connProviderManager; private final PluginService pluginService; private final PluginManagerService pluginManagerService; @@ -66,9 +71,11 @@ public final class DefaultConnectionPlugin implements ConnectionPlugin { public DefaultConnectionPlugin( final PluginService pluginService, final ConnectionProvider defaultConnProvider, + final @Nullable ConnectionProvider effectiveConnProvider, final PluginManagerService pluginManagerService) { this(pluginService, defaultConnProvider, + effectiveConnProvider, pluginManagerService, new ConnectionProviderManager(defaultConnProvider)); } @@ -76,9 +83,9 @@ public DefaultConnectionPlugin( public DefaultConnectionPlugin( final PluginService pluginService, final ConnectionProvider defaultConnProvider, + final @Nullable ConnectionProvider effectiveConnProvider, final PluginManagerService pluginManagerService, final ConnectionProviderManager connProviderManager) { - if (pluginService == null) { throw new IllegalArgumentException("pluginService"); } @@ -91,6 +98,8 @@ public DefaultConnectionPlugin( this.pluginService = pluginService; this.pluginManagerService = pluginManagerService; + this.defaultConnProvider = defaultConnProvider; + this.effectiveConnProvider = effectiveConnProvider; this.connProviderManager = connProviderManager; } @@ -163,8 +172,19 @@ public Connection connect( final boolean isInitialConnection, final JdbcCallable connectFunc) throws SQLException { - final ConnectionProvider connProvider = - this.connProviderManager.getConnectionProvider(driverProtocol, hostSpec, props); + + ConnectionProvider connProvider = null; + + if (this.effectiveConnProvider != null) { + if (this.effectiveConnProvider.acceptsUrl(driverProtocol, hostSpec, props)) { + connProvider = this.effectiveConnProvider; + } + } + + if (connProvider == null) { + connProvider = + this.connProviderManager.getConnectionProvider(driverProtocol, hostSpec, props); + } // It's guaranteed that this plugin is always the last in plugin chain so connectFunc can be // ignored. @@ -180,7 +200,12 @@ private Connection connectInternal( Connection conn; try { - conn = connProvider.connect(driverProtocol, this.pluginService.getDialect(), hostSpec, props); + conn = connProvider.connect( + driverProtocol, + this.pluginService.getDialect(), + this.pluginService.getTargetDriverDialect(), + hostSpec, + props); } finally { telemetryContext.closeContext(); } @@ -201,12 +226,10 @@ public Connection forceConnect( final boolean isInitialConnection, final JdbcCallable forceConnectFunc) throws SQLException { - final ConnectionProvider connProvider = - this.connProviderManager.getDefaultProvider(); // It's guaranteed that this plugin is always the last in plugin chain so forceConnectFunc can be // ignored. - return connectInternal(driverProtocol, hostSpec, props, connProvider); + return connectInternal(driverProtocol, hostSpec, props, this.defaultConnProvider); } @Override @@ -215,6 +238,10 @@ public boolean acceptsStrategy(HostRole role, String strategy) { // Users must request either a writer or a reader role. return false; } + + if (this.effectiveConnProvider != null) { + return this.effectiveConnProvider.acceptsStrategy(role, strategy); + } return this.connProviderManager.acceptsStrategy(role, strategy); } @@ -231,6 +258,10 @@ public HostSpec getHostSpecByStrategy(HostRole role, String strategy) throw new SQLException(Messages.get("DefaultConnectionPlugin.noHostsAvailable")); } + if (this.effectiveConnProvider != null) { + return this.effectiveConnProvider.getHostSpecByStrategy(hosts, + role, strategy, this.pluginService.getProperties()); + } return this.connProviderManager.getHostSpecByStrategy(hosts, role, strategy, this.pluginService.getProperties()); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/IamAuthConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/IamAuthConnectionPlugin.java index 69fa0c813..d56aafaa2 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/IamAuthConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/IamAuthConnectionPlugin.java @@ -36,6 +36,7 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.authentication.AwsCredentialsManager; +import software.amazon.jdbc.util.IamAuthUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUtils; import software.amazon.jdbc.util.StringUtils; @@ -64,7 +65,7 @@ public class IamAuthConnectionPlugin extends AbstractConnectionPlugin { "Overrides the host that is used to generate the IAM token"); public static final AwsWrapperProperty IAM_DEFAULT_PORT = new AwsWrapperProperty( - "iamDefaultPort", null, + "iamDefaultPort", "-1", "Overrides default port that is used to generate the IAM token"); public static final AwsWrapperProperty IAM_REGION = new AwsWrapperProperty( @@ -115,12 +116,12 @@ private Connection connectInternal(String driverProtocol, HostSpec hostSpec, Pro throw new SQLException(PropertyDefinition.USER.name + " is null or empty."); } - String host = hostSpec.getHost(); - if (!StringUtils.isNullOrEmpty(IAM_HOST.getString(props))) { - host = IAM_HOST.getString(props); - } + String host = IamAuthUtils.getIamHost(IAM_HOST.getString(props), hostSpec); - int port = getPort(props, hostSpec); + int port = IamAuthUtils.getIamPort( + IAM_DEFAULT_PORT.getInteger(props), + hostSpec, + this.pluginService.getDialect().getDefaultPort()); final String iamRegion = IAM_REGION.getString(props); final Region region = StringUtils.isNullOrEmpty(iamRegion) @@ -261,26 +262,6 @@ public static void clearCache() { tokenCache.clear(); } - private int getPort(Properties props, HostSpec hostSpec) { - if (!StringUtils.isNullOrEmpty(IAM_DEFAULT_PORT.getString(props))) { - int defaultPort = IAM_DEFAULT_PORT.getInteger(props); - if (defaultPort > 0) { - return defaultPort; - } else { - LOGGER.finest( - () -> Messages.get( - "IamAuthConnectionPlugin.invalidPort", - new Object[] {defaultPort})); - } - } - - if (hostSpec.isPortSpecified()) { - return hostSpec.getPort(); - } else { - return this.pluginService.getDialect().getDefaultPort(); - } - } - private Region getRdsRegion(final String hostname) throws SQLException { // Get Region @@ -312,27 +293,4 @@ private Region getRdsRegion(final String hostname) throws SQLException { return regionOptional.get(); } - - static class TokenInfo { - - private final String token; - private final Instant expiration; - - public TokenInfo(final String token, final Instant expiration) { - this.token = token; - this.expiration = expiration; - } - - public String getToken() { - return this.token; - } - - public Instant getExpiration() { - return this.expiration; - } - - public boolean isExpired() { - return Instant.now().isAfter(this.expiration); - } - } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/OpenedConnectionTracker.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/OpenedConnectionTracker.java index 39992e839..5c0a30790 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/OpenedConnectionTracker.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/OpenedConnectionTracker.java @@ -181,7 +181,7 @@ private void logConnectionQueue(final String host, final Queue connection : queue) { builder.append("\n\t").append(connection.get()); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/TokenInfo.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/TokenInfo.java new file mode 100644 index 000000000..3fad01093 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/TokenInfo.java @@ -0,0 +1,41 @@ +/* + * 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.plugin; + +import java.time.Instant; + +public class TokenInfo { + private final String token; + private final Instant expiration; + + public TokenInfo(final String token, final Instant expiration) { + this.token = token; + this.expiration = expiration; + } + + public String getToken() { + return this.token; + } + + public Instant getExpiration() { + return this.expiration; + } + + public boolean isExpired() { + return Instant.now().isAfter(this.expiration); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/HostMonitoringConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/HostMonitoringConnectionPlugin.java index 39053ef16..7305250f0 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/HostMonitoringConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/HostMonitoringConnectionPlugin.java @@ -82,7 +82,6 @@ public class HostMonitoringConnectionPlugin extends AbstractConnectionPlugin protected @NonNull Properties properties; private final @NonNull Supplier monitorServiceSupplier; private final @NonNull PluginService pluginService; - private final @NonNull TelemetryFactory telemetryFactory; private MonitorService monitorService; private final RdsUtils rdsHelper; private HostSpec monitoringHostSpec; @@ -119,7 +118,6 @@ public HostMonitoringConnectionPlugin( throw new IllegalArgumentException("monitorServiceSupplier"); } this.pluginService = pluginService; - this.telemetryFactory = pluginService.getTelemetryFactory(); this.properties = properties; this.monitorServiceSupplier = monitorServiceSupplier; this.rdsHelper = rdsHelper; @@ -167,11 +165,13 @@ public T execute( "HostMonitoringConnectionPlugin.activatedMonitoring", new Object[] {methodName})); + final HostSpec monitoringHostSpec = this.getMonitoringHostSpec(); + monitorContext = this.monitorService.startMonitoring( this.pluginService.getCurrentConnection(), // abort this connection if needed - this.getMonitoringHostSpec().asAliases(), - this.getMonitoringHostSpec(), + monitoringHostSpec.asAliases(), + monitoringHostSpec, this.properties, failureDetectionTimeMillis, failureDetectionIntervalMillis, diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/MonitorImpl.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/MonitorImpl.java index e7a83d7e5..02bb3f03f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/MonitorImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm/MonitorImpl.java @@ -252,9 +252,9 @@ public void run() { throw intEx; } catch (final Exception ex) { // log and ignore - if (LOGGER.isLoggable(Level.WARNING)) { + if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log( - Level.WARNING, + Level.FINEST, Messages.get( "MonitorImpl.exceptionDuringMonitoringContinue", new Object[]{this.hostSpec.getHost()}), @@ -270,9 +270,9 @@ public void run() { new Object[] {this.hostSpec.getHost()})); } catch (final Exception ex) { // this should not be reached; log and exit thread - if (LOGGER.isLoggable(Level.WARNING)) { + if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log( - Level.WARNING, + Level.FINEST, Messages.get( "MonitorImpl.exceptionDuringMonitoringStop", new Object[]{this.hostSpec.getHost()}), diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPlugin.java new file mode 100644 index 000000000..d54f3b623 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPlugin.java @@ -0,0 +1,285 @@ +/* + * 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.plugin.efm2; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.AwsWrapperProperty; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.JdbcCallable; +import software.amazon.jdbc.NodeChangeOptions; +import software.amazon.jdbc.OldConnectionSuggestedAction; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.cleanup.CanReleaseResources; +import software.amazon.jdbc.plugin.AbstractConnectionPlugin; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.RdsUrlType; +import software.amazon.jdbc.util.RdsUtils; +import software.amazon.jdbc.util.SubscribedMethodHelper; + +/** + * Monitor the server while the connection is executing methods for more sophisticated failure + * detection. + */ +public class HostMonitoringConnectionPlugin extends AbstractConnectionPlugin + implements CanReleaseResources { + + private static final Logger LOGGER = + Logger.getLogger(HostMonitoringConnectionPlugin.class.getName()); + + public static final AwsWrapperProperty FAILURE_DETECTION_ENABLED = + new AwsWrapperProperty( + "failureDetectionEnabled", + "true", + "Enable failure detection logic (aka node monitoring thread)."); + + public static final AwsWrapperProperty FAILURE_DETECTION_TIME = + new AwsWrapperProperty( + "failureDetectionTime", + "30000", + "Interval in millis between sending SQL to the server and the first probe to database node."); + + public static final AwsWrapperProperty FAILURE_DETECTION_INTERVAL = + new AwsWrapperProperty( + "failureDetectionInterval", + "5000", + "Interval in millis between probes to database node."); + + public static final AwsWrapperProperty FAILURE_DETECTION_COUNT = + new AwsWrapperProperty( + "failureDetectionCount", + "3", + "Number of failed connection checks before considering database node unhealthy."); + + private static final Set subscribedMethods = + Collections.unmodifiableSet(new HashSet<>(Collections.singletonList("*"))); + + protected @NonNull Properties properties; + private final @NonNull Supplier monitorServiceSupplier; + private final @NonNull PluginService pluginService; + private MonitorService monitorService; + private final RdsUtils rdsHelper; + private HostSpec monitoringHostSpec; + + static { + PropertyDefinition.registerPluginProperties(HostMonitoringConnectionPlugin.class); + PropertyDefinition.registerPluginProperties("monitoring-"); + } + + /** + * Initialize the node monitoring plugin. + * + * @param pluginService A service allowing the plugin to retrieve the current active connection + * and its connection settings. + * @param properties The property set used to initialize the active connection. + */ + public HostMonitoringConnectionPlugin( + final @NonNull PluginService pluginService, final @NonNull Properties properties) { + this(pluginService, properties, () -> new MonitorServiceImpl(pluginService), new RdsUtils()); + } + + HostMonitoringConnectionPlugin( + final @NonNull PluginService pluginService, + final @NonNull Properties properties, + final @NonNull Supplier monitorServiceSupplier, + final RdsUtils rdsHelper) { + if (pluginService == null) { + throw new IllegalArgumentException("pluginService"); + } + if (properties == null) { + throw new IllegalArgumentException("properties"); + } + if (monitorServiceSupplier == null) { + throw new IllegalArgumentException("monitorServiceSupplier"); + } + this.pluginService = pluginService; + this.properties = properties; + this.monitorServiceSupplier = monitorServiceSupplier; + this.rdsHelper = rdsHelper; + } + + @Override + public Set getSubscribedMethods() { + return subscribedMethods; + } + + /** + * Executes the given SQL function with {@link MonitorImpl} if connection monitoring is enabled. + * Otherwise, executes the SQL function directly. + */ + @Override + public T execute( + final Class resultClass, + final Class exceptionClass, + final Object methodInvokeOn, + final String methodName, + final JdbcCallable jdbcMethodFunc, + final Object[] jdbcMethodArgs) + throws E { + + // update config settings since they may change + final boolean isEnabled = FAILURE_DETECTION_ENABLED.getBoolean(this.properties); + + if (!isEnabled || !SubscribedMethodHelper.NETWORK_BOUND_METHODS.contains(methodName)) { + return jdbcMethodFunc.call(); + } + + final int failureDetectionTimeMillis = FAILURE_DETECTION_TIME.getInteger(this.properties); + final int failureDetectionIntervalMillis = + FAILURE_DETECTION_INTERVAL.getInteger(this.properties); + final int failureDetectionCount = FAILURE_DETECTION_COUNT.getInteger(this.properties); + + initMonitorService(); + + T result; + MonitorConnectionContext monitorContext = null; + + try { + LOGGER.finest( + () -> Messages.get( + "HostMonitoringConnectionPlugin.activatedMonitoring", + new Object[] {methodName})); + + final HostSpec monitoringHostSpec = this.getMonitoringHostSpec(); + + monitorContext = + this.monitorService.startMonitoring( + this.pluginService.getCurrentConnection(), // abort this connection if needed + monitoringHostSpec, + this.properties, + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount); + + result = jdbcMethodFunc.call(); + + } finally { + if (monitorContext != null) { + this.monitorService.stopMonitoring(monitorContext, this.pluginService.getCurrentConnection()); + } + + LOGGER.finest( + () -> Messages.get( + "HostMonitoringConnectionPlugin.monitoringDeactivated", + new Object[] {methodName})); + } + + return result; + } + + private void initMonitorService() { + if (this.monitorService == null) { + this.monitorService = this.monitorServiceSupplier.get(); + } + } + + /** Call this plugin's monitor service to release all resources associated with this plugin. */ + @Override + public void releaseResources() { + if (this.monitorService != null) { + this.monitorService.releaseResources(); + } + + this.monitorService = null; + } + + @Override + public OldConnectionSuggestedAction notifyConnectionChanged(final EnumSet changes) { + if (changes.contains(NodeChangeOptions.HOSTNAME) + || changes.contains(NodeChangeOptions.NODE_CHANGED)) { + + // Reset monitoring HostSpec since the associated connection has changed. + this.monitoringHostSpec = null; + } + + return OldConnectionSuggestedAction.NO_OPINION; + } + + @Override + public Connection connect( + final @NonNull String driverProtocol, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props, + final boolean isInitialConnection, + final @NonNull JdbcCallable connectFunc) + throws SQLException { + return connectInternal(driverProtocol, hostSpec, connectFunc); + } + + private Connection connectInternal(String driverProtocol, HostSpec hostSpec, + JdbcCallable connectFunc) throws SQLException { + final Connection conn = connectFunc.call(); + + if (conn != null) { + final RdsUrlType type = this.rdsHelper.identifyRdsType(hostSpec.getHost()); + if (type.isRdsCluster()) { + hostSpec.resetAliases(); + this.pluginService.fillAliases(conn, hostSpec); + } + } + + return conn; + } + + @Override + public Connection forceConnect( + final @NonNull String driverProtocol, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props, + final boolean isInitialConnection, + final @NonNull JdbcCallable forceConnectFunc) + throws SQLException { + return connectInternal(driverProtocol, hostSpec, forceConnectFunc); + } + + public HostSpec getMonitoringHostSpec() { + if (this.monitoringHostSpec == null) { + this.monitoringHostSpec = this.pluginService.getCurrentHostSpec(); + final RdsUrlType rdsUrlType = this.rdsHelper.identifyRdsType(monitoringHostSpec.getUrl()); + + try { + if (rdsUrlType.isRdsCluster()) { + LOGGER.finest("Monitoring HostSpec is associated with a cluster endpoint, " + + "plugin needs to identify the cluster connection."); + this.monitoringHostSpec = this.pluginService.identifyConnection(this.pluginService.getCurrentConnection()); + if (this.monitoringHostSpec == null) { + throw new RuntimeException(Messages.get( + "HostMonitoringConnectionPlugin.unableToIdentifyConnection", + new Object[] { + this.pluginService.getCurrentHostSpec().getHost(), + this.pluginService.getHostListProvider()})); + } + this.pluginService.fillAliases(this.pluginService.getCurrentConnection(), monitoringHostSpec); + } + } catch (SQLException e) { + // Log and throw. + LOGGER.finest(Messages.get("HostMonitoringConnectionPlugin.errorIdentifyingConnection", new Object[] {e})); + throw new RuntimeException(e); + } + } + return this.monitoringHostSpec; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPluginFactory.java new file mode 100644 index 000000000..0dfdb79ca --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/HostMonitoringConnectionPluginFactory.java @@ -0,0 +1,30 @@ +/* + * 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.plugin.efm2; + +import java.util.Properties; +import software.amazon.jdbc.ConnectionPlugin; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.PluginService; + +/** Class initializing a {@link HostMonitoringConnectionPlugin}. */ +public class HostMonitoringConnectionPluginFactory implements ConnectionPluginFactory { + @Override + public ConnectionPlugin getInstance(final PluginService pluginService, final Properties props) { + return new HostMonitoringConnectionPlugin(pluginService, props); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/Monitor.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/Monitor.java new file mode 100644 index 000000000..5689db2ce --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/Monitor.java @@ -0,0 +1,28 @@ +/* + * 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.plugin.efm2; + +/** + * Interface for monitors. This class uses background threads to monitor servers with one or more + * connections for more efficient failure detection during method execution. + */ +public interface Monitor extends AutoCloseable, Runnable { + + void startMonitoring(MonitorConnectionContext context); + + boolean canDispose(); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorConnectionContext.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorConnectionContext.java new file mode 100644 index 000000000..806047147 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorConnectionContext.java @@ -0,0 +1,67 @@ +/* + * 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.plugin.efm2; + +import java.lang.ref.WeakReference; +import java.sql.Connection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Monitoring context for each connection. This contains each connection's criteria for whether a + * server should be considered unhealthy. The context is shared between the main thread and the monitor thread. + */ +public class MonitorConnectionContext { + + private final AtomicReference> connectionToAbortRef; + private final AtomicBoolean nodeUnhealthy = new AtomicBoolean(false); + + /** + * Constructor. + * + * @param connectionToAbort A reference to the connection associated with this context that will be aborted. + */ + public MonitorConnectionContext(final Connection connectionToAbort) { + this.connectionToAbortRef = new AtomicReference<>(new WeakReference<>(connectionToAbort)); + } + + public boolean isNodeUnhealthy() { + return this.nodeUnhealthy.get(); + } + + void setNodeUnhealthy(final boolean nodeUnhealthy) { + this.nodeUnhealthy.set(nodeUnhealthy); + } + + public boolean shouldAbort() { + return this.nodeUnhealthy.get() && this.connectionToAbortRef.get() != null; + } + + public void setInactive() { + this.connectionToAbortRef.set(null); + } + + public Connection getConnection() { + WeakReference copy = this.connectionToAbortRef.get(); + return copy == null ? null : copy.get(); + } + + public boolean isActive() { + WeakReference copy = this.connectionToAbortRef.get(); + return copy != null && copy.get() != null; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorImpl.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorImpl.java new file mode 100644 index 000000000..61090c078 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorImpl.java @@ -0,0 +1,420 @@ +/* + * 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.plugin.efm2; + +import java.lang.ref.WeakReference; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.PropertyUtils; +import software.amazon.jdbc.util.StringUtils; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryCounter; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; +import software.amazon.jdbc.util.telemetry.TelemetryGauge; +import software.amazon.jdbc.util.telemetry.TelemetryTraceLevel; + +/** + * This class uses a background thread to monitor a particular server with one or more active {@link + * Connection}. + */ +public class MonitorImpl implements Monitor { + + private static final Logger LOGGER = Logger.getLogger(MonitorImpl.class.getName()); + private static final long THREAD_SLEEP_NANO = TimeUnit.SECONDS.toNanos(1); + private static final String MONITORING_PROPERTY_PREFIX = "monitoring-"; + + protected static final Executor ABORT_EXECUTOR = Executors.newSingleThreadExecutor(); + + private final Queue> activeContexts = new ConcurrentLinkedQueue<>(); + private final HashMap>> newContexts = new HashMap<>(); + private final PluginService pluginService; + private final TelemetryFactory telemetryFactory; + private final Properties properties; + private final HostSpec hostSpec; + private final AtomicBoolean stopped = new AtomicBoolean(false); + private Connection monitoringConn = null; + private final ExecutorService threadPool = Executors.newFixedThreadPool(2, runnableTarget -> { + final Thread monitoringThread = new Thread(runnableTarget); + monitoringThread.setDaemon(true); + return monitoringThread; + }); + + private final long failureDetectionTimeNano; + private final long failureDetectionIntervalNano; + private final int failureDetectionCount; + + private long invalidNodeStartTimeNano; + private long failureCount; + private boolean nodeUnhealthy = false; + + + private final TelemetryGauge newContextsSizeGauge; + private final TelemetryGauge activeContextsSizeGauge; + private final TelemetryGauge nodeHealtyGauge; + private final TelemetryCounter abortedConnectionsCounter; + + /** + * Store the monitoring configuration for a connection. + * + * @param pluginService A service for creating new connections. + * @param hostSpec The {@link HostSpec} of the server this {@link MonitorImpl} + * instance is monitoring. + * @param properties The {@link Properties} containing additional monitoring + * configuration. + */ + public MonitorImpl( + final @NonNull PluginService pluginService, + final @NonNull HostSpec hostSpec, + final @NonNull Properties properties, + final int failureDetectionTimeMillis, + final int failureDetectionIntervalMillis, + final int failureDetectionCount, + final TelemetryCounter abortedConnectionsCounter) { + + this.pluginService = pluginService; + this.telemetryFactory = pluginService.getTelemetryFactory(); + this.hostSpec = hostSpec; + this.properties = properties; + this.failureDetectionTimeNano = TimeUnit.MILLISECONDS.toNanos(failureDetectionTimeMillis); + this.failureDetectionIntervalNano = TimeUnit.MILLISECONDS.toNanos(failureDetectionIntervalMillis); + this.failureDetectionCount = failureDetectionCount; + this.abortedConnectionsCounter = abortedConnectionsCounter; + + final String hostId = StringUtils.isNullOrEmpty(this.hostSpec.getHostId()) + ? this.hostSpec.getHost() + : this.hostSpec.getHostId(); + + this.newContextsSizeGauge = telemetryFactory.createGauge( + String.format("efm2.newContexts.size.%s", hostId), + this::getActiveContextSize); + + this.activeContextsSizeGauge = telemetryFactory.createGauge( + String.format("efm2.activeContexts.size.%s", hostId), + () -> (long) this.activeContexts.size()); + + this.nodeHealtyGauge = telemetryFactory.createGauge( + String.format("efm2.nodeHealthy.%s", hostId), + () -> this.nodeUnhealthy ? 0L : 1L); + + this.threadPool.submit(this::newContextRun); // task to handle new contexts + this.threadPool.submit(this); // task to handle active monitoring contexts + this.threadPool.shutdown(); // No more tasks are accepted by pool. + } + + @Override + public boolean canDispose() { + return this.activeContexts.isEmpty() && this.newContexts.isEmpty(); + } + + @Override + public void close() throws Exception { + this.stopped.set(true); + + // Waiting for 30s gives a thread enough time to exit monitoring loop and close database connection. + if (!this.threadPool.awaitTermination(30, TimeUnit.SECONDS)) { + this.threadPool.shutdownNow(); + } + LOGGER.finest(() -> Messages.get( + "MonitorImpl.stopped", + new Object[] {this.hostSpec.getHost()})); + } + + protected long getActiveContextSize() { + return this.newContexts.values().stream().mapToLong(java.util.Collection::size).sum(); + } + + @Override + public void startMonitoring(final MonitorConnectionContext context) { + if (this.stopped.get()) { + LOGGER.warning(() -> Messages.get("MonitorImpl.monitorIsStopped", new Object[] {this.hostSpec.getHost()})); + } + + final long currentTimeNano = this.getCurrentTimeNano(); + long startMonitoringTimeNano = this.truncateNanoToSeconds( + currentTimeNano + this.failureDetectionTimeNano); + + Queue> queue = + this.newContexts.computeIfAbsent( + startMonitoringTimeNano, + (key) -> new ConcurrentLinkedQueue<>()); + queue.add(new WeakReference<>(context)); + } + + private long truncateNanoToSeconds(final long timeNano) { + return TimeUnit.SECONDS.toNanos(TimeUnit.NANOSECONDS.toSeconds(timeNano)); + } + + public void clearContexts() { + this.newContexts.clear(); + this.activeContexts.clear(); + } + + // This method helps to organize unit tests. + long getCurrentTimeNano() { + return System.nanoTime(); + } + + public void newContextRun() { + + final TelemetryContext telemetryContext = telemetryFactory.openTelemetryContext( + "monitoring thread (new contexts)", TelemetryTraceLevel.TOP_LEVEL); + telemetryContext.setAttribute("url", this.hostSpec.getUrl()); + + try { + while (!this.stopped.get()) { + + final long currentTimeNano = this.getCurrentTimeNano(); + + final ArrayList processedKeys = new ArrayList<>(); + this.newContexts.entrySet().stream() + // Get entries with key (that is a time in nanos) less or equal than current time. + .filter(entry -> entry.getKey() < currentTimeNano) + .forEach(entry -> { + final Queue> queue = entry.getValue(); + processedKeys.add(entry.getKey()); + // Each value of found entry is a queue of monitoring contexts awaiting active monitoring. + // Add all contexts to an active monitoring contexts queue. + // Ignore disposed contexts. + WeakReference contextWeakRef; + while ((contextWeakRef = queue.poll()) != null) { + MonitorConnectionContext context = contextWeakRef.get(); + if (context != null && context.isActive()) { + this.activeContexts.add(contextWeakRef); + } + } + }); + processedKeys.forEach(this.newContexts::remove); + + TimeUnit.SECONDS.sleep(1); + } + } catch (final InterruptedException intEx) { + // do nothing; just exit the thread + } catch (final Exception ex) { + // this should not be reached; log and exit thread + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log( + Level.FINEST, + Messages.get( + "MonitorImpl.exceptionDuringMonitoringStop", + new Object[]{this.hostSpec.getHost()}), + ex); // We want to print full trace stack of the exception. + } + } finally { + telemetryContext.closeContext(); + } + } + + @Override + public void run() { + final TelemetryContext telemetryContext = telemetryFactory.openTelemetryContext( + "monitoring thread", TelemetryTraceLevel.TOP_LEVEL); + telemetryContext.setAttribute("url", hostSpec.getUrl()); + + try { + while (!this.stopped.get()) { + + if (this.activeContexts.isEmpty()) { + TimeUnit.NANOSECONDS.sleep(THREAD_SLEEP_NANO); + continue; + } + + final long statusCheckStartTimeNano = this.getCurrentTimeNano(); + final boolean isValid = this.checkConnectionStatus(); + final long statusCheckEndTimeNano = this.getCurrentTimeNano(); + + this.updateNodeHealthStatus(isValid, statusCheckStartTimeNano, statusCheckEndTimeNano); + + if (this.nodeUnhealthy) { + this.pluginService.setAvailability(this.hostSpec.asAliases(), HostAvailability.NOT_AVAILABLE); + } + + final List> tmpActiveContexts = new ArrayList<>(); + WeakReference monitorContextWeakRef; + + while ((monitorContextWeakRef = this.activeContexts.poll()) != null) { + if (this.stopped.get()) { + break; + } + + MonitorConnectionContext monitorContext = monitorContextWeakRef.get(); + if (monitorContext == null) { + continue; + } + + if (this.nodeUnhealthy) { + // Kill connection. + monitorContext.setNodeUnhealthy(true); + final Connection connectionToAbort = monitorContext.getConnection(); + monitorContext.setInactive(); + if (connectionToAbort != null) { + this.abortConnection(connectionToAbort); + this.abortedConnectionsCounter.inc(); + } + } else if (monitorContext.isActive()) { + tmpActiveContexts.add(monitorContextWeakRef); + } + } + + // activeContexts is empty now and tmpActiveContexts contains all yet active contexts + // Add active contexts back to the queue. + this.activeContexts.addAll(tmpActiveContexts); + + long delayNano = this.failureDetectionIntervalNano - (statusCheckEndTimeNano - statusCheckStartTimeNano); + if (delayNano < THREAD_SLEEP_NANO) { + delayNano = THREAD_SLEEP_NANO; + } + TimeUnit.NANOSECONDS.sleep(delayNano); + } + } catch (final InterruptedException intEx) { + // do nothing + } catch (final Exception ex) { + // this should not be reached; log and exit thread + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log( + Level.FINEST, + Messages.get( + "MonitorImpl.exceptionDuringMonitoringStop", + new Object[]{this.hostSpec.getHost()}), + ex); // We want to print full trace stack of the exception. + } + } finally { + this.stopped.set(true); + if (this.monitoringConn != null) { + try { + this.monitoringConn.close(); + } catch (final SQLException ex) { + // ignore + } + } + telemetryContext.closeContext(); + } + } + + /** + * Check the status of the monitored server by establishing a connection and sending a ping. + * + * @return True, if the server is still alive. + */ + boolean checkConnectionStatus() { + TelemetryContext connectContext = telemetryFactory.openTelemetryContext( + "connection status check", TelemetryTraceLevel.NESTED); + try { + if (this.monitoringConn == null || this.monitoringConn.isClosed()) { + // open a new connection + final Properties monitoringConnProperties = PropertyUtils.copyProperties(this.properties); + + this.properties.stringPropertyNames().stream() + .filter(p -> p.startsWith(MONITORING_PROPERTY_PREFIX)) + .forEach( + p -> { + monitoringConnProperties.put( + p.substring(MONITORING_PROPERTY_PREFIX.length()), + this.properties.getProperty(p)); + monitoringConnProperties.remove(p); + }); + + LOGGER.finest(() -> "Opening a monitoring connection to " + this.hostSpec.getUrl()); + this.monitoringConn = this.pluginService.forceConnect(this.hostSpec, monitoringConnProperties); + LOGGER.finest(() -> "Opened monitoring connection: " + this.monitoringConn); + return true; + } + + final boolean isValid = this.monitoringConn.isValid( + (int) TimeUnit.NANOSECONDS.toSeconds(this.failureDetectionIntervalNano)); + return isValid; + + } catch (final SQLException sqlEx) { + return false; + + } finally { + connectContext.closeContext(); + } + } + + private void updateNodeHealthStatus( + final boolean connectionValid, + final long statusCheckStartNano, + final long statusCheckEndNano) { + + if (!connectionValid) { + this.failureCount++; + + if (this.invalidNodeStartTimeNano == 0) { + this.invalidNodeStartTimeNano = statusCheckStartNano; + } + + final long invalidNodeDurationNano = statusCheckEndNano - this.invalidNodeStartTimeNano; + final long maxInvalidNodeDurationNano = + this.failureDetectionIntervalNano * Math.max(0, this.failureDetectionCount); + + if (invalidNodeDurationNano >= maxInvalidNodeDurationNano) { + LOGGER.fine(() -> Messages.get("MonitorConnectionContext.hostDead", new Object[] {this.hostSpec.getHost()})); + this.nodeUnhealthy = true; + return; + } + + LOGGER.finest( + () -> Messages.get( + "MonitorConnectionContext.hostNotResponding", + new Object[] {this.hostSpec.getHost(), this.failureCount})); + return; + } + + if (this.failureCount > 0) { + // Node is back alive + LOGGER.finest( + () -> Messages.get("MonitorConnectionContext.hostAlive", + new Object[] {this.hostSpec.getHost()})); + } + + this.failureCount = 0; + this.invalidNodeStartTimeNano = 0; + this.nodeUnhealthy = false; + } + + private void abortConnection(final @NonNull Connection connectionToAbort) { + try { + connectionToAbort.abort(ABORT_EXECUTOR); + connectionToAbort.close(); + } catch (final SQLException sqlEx) { + // ignore + LOGGER.finest( + () -> Messages.get( + "MonitorConnectionContext.exceptionAbortingConnection", + new Object[] {sqlEx.getMessage()})); + } + } + +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorInitializer.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorInitializer.java new file mode 100644 index 000000000..9027ccc5a --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorInitializer.java @@ -0,0 +1,33 @@ +/* + * 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.plugin.efm2; + +import java.util.Properties; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.util.telemetry.TelemetryCounter; + +/** Interface for initialize a new {@link MonitorImpl}. */ +@FunctionalInterface +public interface MonitorInitializer { + Monitor createMonitor( + HostSpec hostSpec, + Properties properties, + final int failureDetectionTimeMillis, + final int failureDetectionIntervalMillis, + final int failureDetectionCount, + final TelemetryCounter abortedConnectionsCounter); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorService.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorService.java new file mode 100644 index 000000000..6fd36bc87 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorService.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.plugin.efm2; + +import java.sql.Connection; +import java.util.Properties; +import software.amazon.jdbc.HostSpec; + +/** + * Interface for monitor services. This class implements ways to start and stop monitoring servers + * when connections are created. + */ +public interface MonitorService { + + MonitorConnectionContext startMonitoring( + Connection connectionToAbort, + HostSpec hostSpec, + Properties properties, + int failureDetectionTimeMillis, + int failureDetectionIntervalMillis, + int failureDetectionCount); + + /** + * Stop monitoring for a connection represented by the given {@link MonitorConnectionContext}. + * Removes the context from the {@link MonitorImpl}. + * + * @param context The {@link MonitorConnectionContext} representing a connection. + */ + void stopMonitoring(MonitorConnectionContext context, Connection connectionToAbort); + + void releaseResources(); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorServiceImpl.java new file mode 100644 index 000000000..5b17fdac1 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/efm2/MonitorServiceImpl.java @@ -0,0 +1,185 @@ +/* + * 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.plugin.efm2; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.AwsWrapperProperty; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.SlidingExpirationCacheWithCleanupThread; +import software.amazon.jdbc.util.telemetry.TelemetryCounter; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; + +/** + * This class handles the creation and clean up of monitoring threads to servers with one or more + * active connections. + */ +public class MonitorServiceImpl implements MonitorService { + + private static final Logger LOGGER = Logger.getLogger(MonitorServiceImpl.class.getName()); + public static final AwsWrapperProperty MONITOR_DISPOSAL_TIME_MS = + new AwsWrapperProperty( + "monitorDisposalTime", + "600000", // 10min + "Interval in milliseconds for a monitor to be considered inactive and to be disposed."); + + protected static final long CACHE_CLEANUP_NANO = TimeUnit.MINUTES.toNanos(1); + + protected static final Executor ABORT_EXECUTOR = Executors.newSingleThreadExecutor(); + + protected static final SlidingExpirationCacheWithCleanupThread monitors = + new SlidingExpirationCacheWithCleanupThread<>( + Monitor::canDispose, + (monitor) -> { + try { + monitor.close(); + } catch (Exception ex) { + // ignore + } + }, + CACHE_CLEANUP_NANO); + + protected final PluginService pluginService; + protected final MonitorInitializer monitorInitializer; + protected final TelemetryFactory telemetryFactory; + protected final TelemetryCounter abortedConnectionsCounter; + + public MonitorServiceImpl(final @NonNull PluginService pluginService) { + this( + pluginService, + (hostSpec, + properties, + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount, + abortedConnectionsCounter) -> + new MonitorImpl( + pluginService, + hostSpec, + properties, + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount, + abortedConnectionsCounter)); + } + + MonitorServiceImpl( + final @NonNull PluginService pluginService, + final @NonNull MonitorInitializer monitorInitializer) { + this.pluginService = pluginService; + this.telemetryFactory = pluginService.getTelemetryFactory(); + this.abortedConnectionsCounter = telemetryFactory.createCounter("efm2.connections.aborted"); + this.monitorInitializer = monitorInitializer; + } + + public static void clearCache() { + monitors.clear(); + } + + @Override + public MonitorConnectionContext startMonitoring( + final Connection connectionToAbort, + final HostSpec hostSpec, + final Properties properties, + final int failureDetectionTimeMillis, + final int failureDetectionIntervalMillis, + final int failureDetectionCount) { + + final Monitor monitor = this.getMonitor( + hostSpec, + properties, + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount); + + final MonitorConnectionContext context = new MonitorConnectionContext(connectionToAbort); + monitor.startMonitoring(context); + + return context; + } + + @Override + public void stopMonitoring( + @NonNull final MonitorConnectionContext context, + @NonNull Connection connectionToAbort) { + + if (context.shouldAbort()) { + context.setInactive(); + try { + connectionToAbort.abort(ABORT_EXECUTOR); + connectionToAbort.close(); + this.abortedConnectionsCounter.inc(); + } catch (final SQLException sqlEx) { + // ignore + LOGGER.finest( + () -> Messages.get( + "MonitorConnectionContext.exceptionAbortingConnection", + new Object[] {sqlEx.getMessage()})); + } + } else { + context.setInactive(); + } + } + + @Override + public void releaseResources() { + // do nothing + } + + /** + * Get or create a {@link MonitorImpl} for a server. + * + * @param hostSpec Information such as hostname of the server. + * @param properties The user configuration for the current connection. + * @return A {@link MonitorImpl} object associated with a specific server. + */ + protected Monitor getMonitor( + final HostSpec hostSpec, + final Properties properties, + final int failureDetectionTimeMillis, + final int failureDetectionIntervalMillis, + final int failureDetectionCount) { + + final String monitorKey = String.format("%d:%d:%d:%s", + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount, + hostSpec.getUrl()); + + final long cacheExpirationNano = TimeUnit.MILLISECONDS.toNanos( + MONITOR_DISPOSAL_TIME_MS.getLong(properties)); + + return monitors.computeIfAbsent( + monitorKey, + (key) -> monitorInitializer.createMonitor( + hostSpec, + properties, + failureDetectionTimeMillis, + failureDetectionIntervalMillis, + failureDetectionCount, + this.abortedConnectionsCounter), + cacheExpirationNano); + } +} 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 f1ac4bfdd..f92d7ef99 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 @@ -147,7 +147,6 @@ private void submitTasks( final List currentTopology, final ExecutorService executorService, final CompletionService completionService) { final HostSpec writerHost = this.getWriter(currentTopology); - this.pluginService.setAvailability(writerHost.asAliases(), HostAvailability.NOT_AVAILABLE); completionService.submit(new ReconnectToWriterHandler(writerHost)); completionService.submit(new WaitForNewWriterHandler( currentTopology, 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 890589021..da24cc4a0 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 @@ -42,10 +42,6 @@ import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsHelper; -import software.amazon.jdbc.states.RestoreSessionStateCallable; -import software.amazon.jdbc.states.SessionDirtyFlag; -import software.amazon.jdbc.states.SessionStateHelper; -import software.amazon.jdbc.states.SessionStateTransferCallable; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; @@ -80,8 +76,6 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { } }); - private static final String METHOD_SET_READ_ONLY = "Connection.setReadOnly"; - private static final String METHOD_SET_AUTO_COMMIT = "Connection.setAutoCommit"; private static final String METHOD_GET_AUTO_COMMIT = "Connection.getAutoCommit"; private static final String METHOD_GET_CATALOG = "Connection.getCatalog"; private static final String METHOD_GET_SCHEMA = "Connection.getSchema"; @@ -90,9 +84,6 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { static final String METHOD_CLOSE = "Connection.close"; static final String METHOD_IS_CLOSED = "Connection.isClosed"; - protected static SessionStateTransferCallable sessionStateTransferCallable; - protected static RestoreSessionStateCallable restoreSessionStateCallable; - private final PluginService pluginService; protected final Properties properties; protected boolean enableFailoverSetting; @@ -100,7 +91,6 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { protected int failoverClusterTopologyRefreshRateMsSetting; protected int failoverWriterReconnectIntervalMsSetting; protected int failoverReaderConnectTimeoutMsSetting; - protected boolean keepSessionStateOnFailover; protected FailoverMode failoverMode; private boolean telemetryFailoverAdditionalTopTraceSetting; @@ -116,8 +106,6 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { private RdsUrlType rdsUrlType; private HostListProviderService hostListProviderService; private final AuroraStaleDnsHelper staleDnsHelper; - private Boolean savedReadOnlyStatus; - private Boolean savedAutoCommitStatus; public static final AwsWrapperProperty FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS = new AwsWrapperProperty( @@ -157,11 +145,6 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { "failoverMode", null, "Set node role to follow during failover."); - public static final AwsWrapperProperty KEEP_SESSION_STATE_ON_FAILOVER = - new AwsWrapperProperty( - "keepSessionStateOnFailover", "false", - "Allow connections to retain a partial previous session state after failover occurs."); - public static final AwsWrapperProperty TELEMETRY_FAILOVER_ADDITIONAL_TOP_TRACE = new AwsWrapperProperty( "telemetryFailoverAdditionalTopTrace", "false", @@ -207,22 +190,6 @@ public FailoverConnectionPlugin(final PluginService pluginService, final Propert this.failoverReaderFailedCounter = telemetryFactory.createCounter("readerFailover.completed.failed.count"); } - public static void setSessionStateTransferFunc(SessionStateTransferCallable callable) { - sessionStateTransferCallable = callable; - } - - public static void resetSessionStateTransferFunc() { - sessionStateTransferCallable = null; - } - - public static void setRestoreSessionStateFunc(RestoreSessionStateCallable callable) { - restoreSessionStateCallable = callable; - } - - public static void resetRestoreSessionStateFunc() { - restoreSessionStateCallable = null; - } - @Override public Set getSubscribedMethods() { return subscribedMethods; @@ -249,14 +216,6 @@ public T execute( } } - if (methodName.equals(METHOD_SET_READ_ONLY) && jdbcMethodArgs != null && jdbcMethodArgs.length > 0) { - this.savedReadOnlyStatus = (Boolean) jdbcMethodArgs[0]; - } - - if (methodName.equals(METHOD_SET_AUTO_COMMIT) && jdbcMethodArgs != null && jdbcMethodArgs.length > 0) { - this.savedAutoCommitStatus = (Boolean) jdbcMethodArgs[0]; - } - T result = null; try { @@ -402,7 +361,6 @@ private void initSettings() { FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS.getInteger(this.properties); this.failoverWriterReconnectIntervalMsSetting = FAILOVER_WRITER_RECONNECT_INTERVAL_MS.getInteger(this.properties); this.failoverReaderConnectTimeoutMsSetting = FAILOVER_READER_CONNECT_TIMEOUT_MS.getInteger(this.properties); - this.keepSessionStateOnFailover = KEEP_SESSION_STATE_ON_FAILOVER.getBoolean(this.properties); this.telemetryFailoverAdditionalTopTraceSetting = TELEMETRY_FAILOVER_ADDITIONAL_TOP_TRACE.getBoolean(this.properties); } @@ -461,6 +419,7 @@ protected void updateTopology(final boolean forceUpdate) throws SQLException { * @return true if the given method is allowed on closed connections */ private boolean allowedOnClosedConnection(final String methodName) { + // TODO: consider to use target driver dialect return methodName.equals(METHOD_GET_AUTO_COMMIT) || methodName.equals(METHOD_GET_CATALOG) || methodName.equals(METHOD_GET_SCHEMA) @@ -482,13 +441,17 @@ private boolean canUpdateTopology(final String methodName) { /** * Connects this dynamic failover connection proxy to the host pointed out by the given host * index. + *

+ * The method assumes that current connection is not setup. If it's not true, a session state + * transfer from the current connection to a new one may be necessary. This should be handled by callee. * * @param host The host. * @throws SQLException if an error occurs */ private void connectTo(final HostSpec host) throws SQLException { try { - switchCurrentConnectionTo(host, createConnectionForHost(host)); + this.pluginService.setCurrentConnection(createConnectionForHost(host), host); + LOGGER.fine( () -> Messages.get( "Failover.establishedConnection", @@ -535,100 +498,6 @@ private boolean shouldAttemptReaderConnection() { return false; } - /** - * Replaces the previous underlying connection by the connection given. State from previous - * connection, if any, is synchronized with the new one. - * - * @param host The host that matches the given connection. - * @param connection The connection instance to switch to. - * @throws SQLException if an error occurs - */ - private void switchCurrentConnectionTo(final HostSpec host, final Connection connection) throws SQLException { - Connection currentConnection = this.pluginService.getCurrentConnection(); - HostSpec currentHostSpec = this.pluginService.getCurrentHostSpec(); - - if (currentConnection != connection) { - transferSessionState(currentConnection, currentHostSpec, connection, host); - invalidateCurrentConnection(); - } - - this.pluginService.setCurrentConnection(connection, host); - - if (this.pluginManagerService != null) { - this.pluginManagerService.setInTransaction(false); - } - } - - /** - * Transfers session state from one connection to another. - * - * @param src The connection to transfer state from - * @param srcHostSpec The connection {@link HostSpec} to transfer state from - * @param dest The connection to transfer state to - * @param destHostSpec The connection {@link HostSpec} to transfer state to - * @throws SQLException if a database access error occurs, this method is called on a closed connection, this - * method is called during a distributed transaction, or this method is called during a - * transaction - */ - protected void transferSessionState( - final Connection src, - final HostSpec srcHostSpec, - final Connection dest, - final HostSpec destHostSpec) throws SQLException { - - if (src == null || dest == null) { - return; - } - - EnumSet sessionState = this.pluginService.getCurrentConnectionState(); - - SessionStateTransferCallable callableCopy = sessionStateTransferCallable; - if (callableCopy != null) { - final boolean isHandled = callableCopy.transferSessionState(sessionState, src, srcHostSpec, dest, destHostSpec); - if (isHandled) { - // Custom function has handled session transfer - return; - } - } - - // Otherwise, lets run default logic. - sessionState = this.pluginService.getCurrentConnectionState(); - final SessionStateHelper helper = new SessionStateHelper(); - helper.transferSessionState(sessionState, src, dest); - } - - /** - * Restores partial session state from saved values to a connection. - * - * @param dest The connection to transfer state to - * @throws SQLException if a database access error occurs, this method is called on a closed connection, this - * method is called during a distributed transaction, or this method is called during a - * transaction - */ - protected void restoreSessionState(final Connection dest) throws SQLException { - if (dest == null) { - return; - } - - final RestoreSessionStateCallable callableCopy = restoreSessionStateCallable; - if (callableCopy != null) { - final boolean isHandled = callableCopy.restoreSessionState( - this.pluginService.getCurrentConnectionState(), - dest, - this.savedReadOnlyStatus, - this.savedAutoCommitStatus - ); - if (isHandled) { - // Custom function has handled everything. - return; - } - } - - // Otherwise, lets run default logic. - final SessionStateHelper helper = new SessionStateHelper(); - helper.restoreSessionState(dest, this.savedReadOnlyStatus, this.savedAutoCommitStatus); - } - private void dealWithOriginalException( final Throwable originalException, final Throwable wrapperException, @@ -687,6 +556,8 @@ protected void dealWithIllegalStateException( * @throws SQLException if an error occurs */ protected synchronized void failover(final HostSpec failedHost) throws SQLException { + this.pluginService.setAvailability(failedHost.asAliases(), HostAvailability.NOT_AVAILABLE); + if (this.failoverMode == FailoverMode.STRICT_WRITER) { failoverWriter(); } else { @@ -740,9 +611,6 @@ protected void failoverReader(final HostSpec failedHostSpec) throws SQLException return; } - if (keepSessionStateOnFailover) { - restoreSessionState(result.getConnection()); - } this.pluginService.setCurrentConnection(result.getConnection(), result.getHost()); this.pluginService.getCurrentHostSpec().removeAlias(oldAliases.toArray(new String[]{})); @@ -797,9 +665,6 @@ protected void failoverWriter() throws SQLException { // successfully re-connected to a writer node final HostSpec writerHostSpec = getWriter(failoverResult.getTopology()); - if (keepSessionStateOnFailover) { - restoreSessionState(failoverResult.getNewConnection()); - } this.pluginService.setCurrentConnection(failoverResult.getNewConnection(), writerHostSpec); LOGGER.fine( @@ -918,12 +783,6 @@ private Connection connectInternal(String driverProtocol, HostSpec hostSpec, Pro this.staleDnsHelper.getVerifiedConnection(isInitialConnection, this.hostListProviderService, driverProtocol, hostSpec, props, connectFunc); - if (this.keepSessionStateOnFailover) { - this.savedReadOnlyStatus = this.savedReadOnlyStatus == null ? conn.isReadOnly() : this.savedReadOnlyStatus; - this.savedAutoCommitStatus = - this.savedAutoCommitStatus == null ? conn.getAutoCommit() : this.savedAutoCommitStatus; - } - if (isInitialConnection) { this.pluginService.refreshHostList(conn); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactory.java new file mode 100644 index 000000000..aa23a8f45 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactory.java @@ -0,0 +1,252 @@ +/* + * 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.plugin.federatedauth; + +import java.io.IOException; +import java.net.URI; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; +import software.amazon.jdbc.util.telemetry.TelemetryTraceLevel; + +public class AdfsCredentialsProviderFactory extends SamlCredentialsProviderFactory { + + public static final String IDP_NAME = "adfs"; + private static final String TELEMETRY_FETCH_SAML = "Fetch ADFS SAML Assertion"; + private static final Pattern INPUT_TAG_PATTERN = Pattern.compile("", Pattern.DOTALL); + private static final Pattern FORM_ACTION_PATTERN = Pattern.compile(" httpClientSupplier; + private TelemetryContext telemetryContext; + + public AdfsCredentialsProviderFactory(final PluginService pluginService, + final Supplier httpClientSupplier) { + this.pluginService = pluginService; + this.telemetryFactory = this.pluginService.getTelemetryFactory(); + this.httpClientSupplier = httpClientSupplier; + } + + @Override + String getSamlAssertion(final @NonNull Properties props) throws SQLException { + this.telemetryContext = telemetryFactory.openTelemetryContext(TELEMETRY_FETCH_SAML, TelemetryTraceLevel.NESTED); + try (final CloseableHttpClient httpClient = httpClientSupplier.get()) { + String uri = getSignInPageUrl(props); + final String signInPageBody = getSignInPageBody(httpClient, uri); + final String action = getFormActionFromHtmlBody(signInPageBody); + + if (!StringUtils.isNullOrEmpty(action) && action.startsWith("/")) { + uri = getFormActionUrl(props, action); + } + + final List params = getParametersFromHtmlBody(signInPageBody, props); + final String content = getFormActionBody(httpClient, uri, params); + + final Matcher matcher = FederatedAuthPlugin.SAML_RESPONSE_PATTERN.matcher(content); + if (!matcher.find()) { + throw new IOException(Messages.get("AdfsCredentialsProviderFactory.failedLogin", new Object[] {content})); + } + + // return SAML Response value + return matcher.group(FederatedAuthPlugin.SAML_RESPONSE_PATTERN_GROUP); + } catch (final IOException e) { + LOGGER.severe(Messages.get("AdfsCredentialsProviderFactory.getSamlAssertionFailed", new Object[] {e})); + this.telemetryContext.setSuccess(false); + this.telemetryContext.setException(e); + throw new SQLException(e); + } finally { + this.telemetryContext.closeContext(); + } + } + + private String getSignInPageBody(final CloseableHttpClient httpClient, final String uri) throws IOException { + LOGGER.finest(Messages.get("AdfsCredentialsProviderFactory.signOnPageUrl", new Object[] {uri})); + validateUrl(uri); + final HttpGet get = new HttpGet(uri); + try (final CloseableHttpResponse resp = httpClient.execute(get)) { + final StatusLine statusLine = resp.getStatusLine(); + // Check HTTP Status Code is 2xx Success + if (statusLine.getStatusCode() / 100 != 2) { + throw new IOException(Messages.get("AdfsCredentialsProviderFactory.signOnPageRequestFailed", + new Object[] { + statusLine.getStatusCode(), + statusLine.getReasonPhrase(), + EntityUtils.toString(resp.getEntity())})); + } + return EntityUtils.toString(resp.getEntity()); + } + } + + private String getFormActionBody(final CloseableHttpClient httpClient, final String uri, + final List params) throws IOException { + LOGGER.finest(Messages.get("AdfsCredentialsProviderFactory.signOnPagePostActionUrl", new Object[] {uri})); + validateUrl(uri); + final HttpPost post = new HttpPost(uri); + post.setEntity(new UrlEncodedFormEntity(params)); + try (final CloseableHttpResponse resp = httpClient.execute(post)) { + final StatusLine statusLine = resp.getStatusLine(); + // Check HTTP Status Code is 2xx Success + if (statusLine.getStatusCode() / 100 != 2) { + throw new IOException(Messages.get("AdfsCredentialsProviderFactory.signOnPagePostActionRequestFailed", + new Object[] { + statusLine.getStatusCode(), + statusLine.getReasonPhrase(), + EntityUtils.toString(resp.getEntity())})); + } + return EntityUtils.toString(resp.getEntity()); + } + } + + private String getSignInPageUrl(final Properties props) { + return "https://" + FederatedAuthPlugin.IDP_ENDPOINT.getString(props) + ':' + + FederatedAuthPlugin.IDP_PORT.getString(props) + "/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=" + + FederatedAuthPlugin.RELAYING_PARTY_ID.getString(props); + } + + private String getFormActionUrl(final Properties props, final String action) { + return "https://" + FederatedAuthPlugin.IDP_ENDPOINT.getString(props) + ':' + + FederatedAuthPlugin.IDP_PORT.getString(props) + action; + } + + private List getInputTagsFromHTML(final String body) { + final Set distinctInputTags = new HashSet<>(); + final List inputTags = new ArrayList<>(); + final Matcher inputTagMatcher = INPUT_TAG_PATTERN.matcher(body); + while (inputTagMatcher.find()) { + final String tag = inputTagMatcher.group(0); + final String tagNameLower = getValueByKey(tag, "name").toLowerCase(); + if (!tagNameLower.isEmpty() && distinctInputTags.add(tagNameLower)) { + inputTags.add(tag); + } + } + return inputTags; + } + + private String getValueByKey(final String input, final String key) { + final Pattern keyValuePattern = Pattern.compile("(" + Pattern.quote(key) + ")\\s*=\\s*\"(.*?)\""); + final Matcher keyValueMatcher = keyValuePattern.matcher(input); + if (keyValueMatcher.find()) { + return escapeHtmlEntity(keyValueMatcher.group(2)); + } + return ""; + } + + private String escapeHtmlEntity(final String html) { + final StringBuilder sb = new StringBuilder(html.length()); + int i = 0; + final int length = html.length(); + while (i < length) { + final char c = html.charAt(i); + if (c != '&') { + sb.append(c); + i++; + continue; + } + + if (html.startsWith("&", i)) { + sb.append('&'); + i += 5; + } else if (html.startsWith("'", i)) { + sb.append('\''); + i += 6; + } else if (html.startsWith(""", i)) { + sb.append('"'); + i += 6; + } else if (html.startsWith("<", i)) { + sb.append('<'); + i += 4; + } else if (html.startsWith(">", i)) { + sb.append('>'); + i += 4; + } else { + sb.append(c); + ++i; + } + } + return sb.toString(); + } + + private List getParametersFromHtmlBody(final String body, final @NonNull Properties props) { + final List parameters = new ArrayList<>(); + for (final String inputTag : getInputTagsFromHTML(body)) { + final String name = getValueByKey(inputTag, "name"); + final String value = getValueByKey(inputTag, "value"); + final String nameLower = name.toLowerCase(); + + if (nameLower.contains("username")) { + parameters.add(new BasicNameValuePair(name, FederatedAuthPlugin.IDP_USERNAME.getString(props))); + } else if (nameLower.contains("authmethod")) { + if (!value.isEmpty()) { + parameters.add(new BasicNameValuePair(name, value)); + } + } else if (nameLower.contains("password")) { + parameters + .add(new BasicNameValuePair(name, FederatedAuthPlugin.IDP_PASSWORD.getString(props))); + } else if (!name.isEmpty()) { + parameters.add(new BasicNameValuePair(name, value)); + } + } + return parameters; + } + + private String getFormActionFromHtmlBody(final String body) { + final Matcher m = FORM_ACTION_PATTERN.matcher(body); + if (m.find()) { + return escapeHtmlEntity(m.group(1)); + } + return null; + } + + private void validateUrl(final String paramString) throws IOException { + + final URI authorizeRequestUrl = URI.create(paramString); + final String errorMessage = Messages.get("AdfsCredentialsProviderFactory.invalidHttpsUrl", + new Object[] {paramString}); + + if (!authorizeRequestUrl.toURL().getProtocol().equalsIgnoreCase("https")) { + throw new IOException(errorMessage); + } + + final Matcher matcher = FederatedAuthPlugin.HTTPS_URL_PATTERN.matcher(paramString); + if (!matcher.find()) { + throw new IOException(errorMessage); + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/CredentialsProviderFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/CredentialsProviderFactory.java new file mode 100644 index 000000000..a43396bf9 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/CredentialsProviderFactory.java @@ -0,0 +1,29 @@ +/* + * 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.plugin.federatedauth; + +import java.io.Closeable; +import java.sql.SQLException; +import java.util.Properties; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +public interface CredentialsProviderFactory { + AwsCredentialsProvider getAwsCredentialsProvider(String host, Region region, final @NonNull Properties props) throws + SQLException; +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPlugin.java new file mode 100644 index 000000000..7def73fec --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPlugin.java @@ -0,0 +1,319 @@ +/* + * 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.plugin.federatedauth; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.jdbc.AwsWrapperProperty; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.JdbcCallable; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.plugin.AbstractConnectionPlugin; +import software.amazon.jdbc.plugin.TokenInfo; +import software.amazon.jdbc.util.IamAuthUtils; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.RdsUtils; +import software.amazon.jdbc.util.StringUtils; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryCounter; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; +import software.amazon.jdbc.util.telemetry.TelemetryGauge; +import software.amazon.jdbc.util.telemetry.TelemetryTraceLevel; + +public class FederatedAuthPlugin extends AbstractConnectionPlugin { + + static final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>(); + private final CredentialsProviderFactory credentialsProviderFactory; + private static final int DEFAULT_TOKEN_EXPIRATION_SEC = 15 * 60 - 30; + private static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 60000; + public static final AwsWrapperProperty IDP_ENDPOINT = new AwsWrapperProperty("idpEndpoint", null, + "The hosting URL of the Identity Provider"); + public static final AwsWrapperProperty IDP_PORT = + new AwsWrapperProperty("idpPort", "443", "The hosting port of Identity Provider"); + public static final AwsWrapperProperty RELAYING_PARTY_ID = + new AwsWrapperProperty("rpIdentifier", "urn:amazon:webservices", "The relaying party identifier"); + public static final AwsWrapperProperty IAM_ROLE_ARN = + new AwsWrapperProperty("iamRoleArn", null, "The ARN of the IAM Role that is to be assumed."); + public static final AwsWrapperProperty IAM_IDP_ARN = + new AwsWrapperProperty("iamIdpArn", null, "The ARN of the Identity Provider"); + public static final AwsWrapperProperty IAM_REGION = new AwsWrapperProperty("iamRegion", null, + "Overrides AWS region that is used to generate the IAM token"); + public static final AwsWrapperProperty IAM_TOKEN_EXPIRATION = new AwsWrapperProperty("iamTokenExpiration", + String.valueOf(DEFAULT_TOKEN_EXPIRATION_SEC), "IAM token cache expiration in seconds"); + public static final AwsWrapperProperty IDP_USERNAME = + new AwsWrapperProperty("idpUsername", null, "The federated user name"); + public static final AwsWrapperProperty IDP_PASSWORD = new AwsWrapperProperty("idpPassword", null, + "The federated user password"); + public static final AwsWrapperProperty IAM_HOST = new AwsWrapperProperty( + "iamHost", null, + "Overrides the host that is used to generate the IAM token"); + public static final AwsWrapperProperty IAM_DEFAULT_PORT = new AwsWrapperProperty("iamDefaultPort", "-1", + "Overrides default port that is used to generate the IAM token"); + public static final AwsWrapperProperty HTTP_CLIENT_SOCKET_TIMEOUT = new AwsWrapperProperty( + "httpClientSocketTimeout", String.valueOf(DEFAULT_HTTP_TIMEOUT_MILLIS), + "The socket timeout value in milliseconds for the HttpClient used by the FederatedAuthPlugin"); + public static final AwsWrapperProperty HTTP_CLIENT_CONNECT_TIMEOUT = new AwsWrapperProperty( + "httpClientConnectTimeout", String.valueOf(DEFAULT_HTTP_TIMEOUT_MILLIS), + "The connect timeout value in milliseconds for the HttpClient used by the FederatedAuthPlugin"); + public static final AwsWrapperProperty SSL_INSECURE = new AwsWrapperProperty("sslInsecure", "true", + "Whether or not the SSL session is to be secure and the sever's certificates will be verified"); + public static AwsWrapperProperty + IDP_NAME = new AwsWrapperProperty("idpName", null, "The name of the Identity Provider implementation used"); + public static final AwsWrapperProperty DB_USER = + new AwsWrapperProperty("dbUser", null, "The database user used to access the database"); + protected static final Pattern SAML_RESPONSE_PATTERN = Pattern.compile("SAMLResponse\\W+value=\"(?[^\"]+)\""); + protected static final String SAML_RESPONSE_PATTERN_GROUP = "saml"; + protected static final Pattern HTTPS_URL_PATTERN = + Pattern.compile("^(https)://[-a-zA-Z0-9+&@#/%?=~_!:,.']*[-a-zA-Z0-9+&@#/%=~_']"); + + private static final String TELEMETRY_FETCH_TOKEN = "fetch IAM token"; + private static final Logger LOGGER = Logger.getLogger(FederatedAuthPlugin.class.getName()); + + protected final PluginService pluginService; + + protected final RdsUtils rdsUtils = new RdsUtils(); + + private static final Set subscribedMethods = + Collections.unmodifiableSet(new HashSet() { + { + add("connect"); + add("forceConnect"); + } + }); + + static { + PropertyDefinition.registerPluginProperties(FederatedAuthPlugin.class); + } + + private final TelemetryFactory telemetryFactory; + private final TelemetryGauge cacheSizeGauge; + private final TelemetryCounter fetchTokenCounter; + + @Override + public Set getSubscribedMethods() { + return subscribedMethods; + } + + public FederatedAuthPlugin(final PluginService pluginService, + final CredentialsProviderFactory credentialsProviderFactory) { + try { + Class.forName("software.amazon.awssdk.services.sts.model.AssumeRoleWithSamlRequest"); + } catch (final ClassNotFoundException e) { + throw new RuntimeException(Messages.get("FederatedAuthPlugin.javaStsSdkNotInClasspath")); + } + this.pluginService = pluginService; + this.credentialsProviderFactory = credentialsProviderFactory; + this.telemetryFactory = pluginService.getTelemetryFactory(); + this.cacheSizeGauge = telemetryFactory.createGauge("federatedAuth.tokenCache.size", () -> (long) tokenCache.size()); + this.fetchTokenCounter = telemetryFactory.createCounter("federatedAuth.fetchToken.count"); + } + + + @Override + public Connection connect( + final String driverProtocol, + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + return connectInternal(hostSpec, props, connectFunc); + } + + @Override + public Connection forceConnect( + final @NonNull String driverProtocol, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props, + final boolean isInitialConnection, + final @NonNull JdbcCallable forceConnectFunc) + throws SQLException { + return connectInternal(hostSpec, props, forceConnectFunc); + } + + private Connection connectInternal(final HostSpec hostSpec, final Properties props, + final JdbcCallable connectFunc) throws SQLException { + + checkIdpCredentialsWithFallback(props); + + final String host = IamAuthUtils.getIamHost(IAM_HOST.getString(props), hostSpec); + + final int port = IamAuthUtils.getIamPort( + IAM_DEFAULT_PORT.getInteger(props), + hostSpec, + this.pluginService.getDialect().getDefaultPort()); + + final Region region = getRegion(host, props); + + final String cacheKey = getCacheKey( + DB_USER.getString(props), + host, + port, + region); + + final TokenInfo tokenInfo = tokenCache.get(cacheKey); + + final boolean isCachedToken = tokenInfo != null && !tokenInfo.isExpired(); + + if (isCachedToken) { + LOGGER.finest( + () -> Messages.get( + "FederatedAuthPlugin.useCachedIamToken", + new Object[] {tokenInfo.getToken()})); + PropertyDefinition.PASSWORD.set(props, tokenInfo.getToken()); + } else { + updateAuthenticationToken(hostSpec, props, region, cacheKey); + } + + PropertyDefinition.USER.set(props, DB_USER.getString(props)); + + try { + return connectFunc.call(); + } catch (final SQLException exception) { + updateAuthenticationToken(hostSpec, props, region, cacheKey); + return connectFunc.call(); + } catch (final Exception exception) { + LOGGER.warning( + () -> Messages.get( + "FederatedAuthPlugin.unhandledException", + new Object[] {exception})); + throw new SQLException(exception); + } + } + + private void checkIdpCredentialsWithFallback(final Properties props) { + if (IDP_USERNAME.getString(props) == null) { + IDP_USERNAME.set(props, PropertyDefinition.USER.getString(props)); + } + + if (IDP_PASSWORD.getString(props) == null) { + IDP_PASSWORD.set(props, PropertyDefinition.PASSWORD.getString(props)); + } + } + + private void updateAuthenticationToken(final HostSpec hostSpec, final Properties props, final Region region, + final String cacheKey) + throws SQLException { + final int tokenExpirationSec = IAM_TOKEN_EXPIRATION.getInteger(props); + final Instant tokenExpiry = Instant.now().plus(tokenExpirationSec, ChronoUnit.SECONDS); + final int port = IamAuthUtils.getIamPort( + StringUtils.isNullOrEmpty(IAM_DEFAULT_PORT.getString(props)) ? 0 : IAM_DEFAULT_PORT.getInteger(props), + hostSpec, + this.pluginService.getDialect().getDefaultPort()); + final AwsCredentialsProvider credentialsProvider = + this.credentialsProviderFactory.getAwsCredentialsProvider(hostSpec.getHost(), region, props); + final String token = generateAuthenticationToken( + props, + hostSpec.getHost(), + port, + region, + credentialsProvider); + LOGGER.finest( + () -> Messages.get( + "FederatedAuthPlugin.generatedNewIamToken", + new Object[] {token})); + PropertyDefinition.PASSWORD.set(props, token); + tokenCache.put( + cacheKey, + new TokenInfo(token, tokenExpiry)); + } + + private Region getRegion(final String hostname, final Properties props) throws SQLException { + final String iamRegion = IAM_REGION.getString(props); + if (!StringUtils.isNullOrEmpty(iamRegion)) { + return Region.of(iamRegion); + } + + // Fallback to using host + // Get Region + final String rdsRegion = rdsUtils.getRdsRegion(hostname); + + if (StringUtils.isNullOrEmpty(rdsRegion)) { + // Does not match Amazon's Hostname, throw exception + final String exceptionMessage = Messages.get( + "FederatedAuthPlugin.unsupportedHostname", + new Object[] {hostname}); + + LOGGER.fine(exceptionMessage); + throw new SQLException(exceptionMessage); + } + + // Check Region + final Optional regionOptional = Region.regions().stream() + .filter(r -> r.id().equalsIgnoreCase(rdsRegion)) + .findFirst(); + + if (!regionOptional.isPresent()) { + final String exceptionMessage = Messages.get( + "AwsSdk.unsupportedRegion", + new Object[] {rdsRegion}); + + LOGGER.fine(exceptionMessage); + throw new SQLException(exceptionMessage); + } + + return regionOptional.get(); + } + + String generateAuthenticationToken(final Properties props, final String hostname, + final int port, final Region region, final AwsCredentialsProvider awsCredentialsProvider) { + final TelemetryFactory telemetryFactory = this.pluginService.getTelemetryFactory(); + final TelemetryContext telemetryContext = telemetryFactory.openTelemetryContext( + TELEMETRY_FETCH_TOKEN, TelemetryTraceLevel.NESTED); + this.fetchTokenCounter.inc(); + try { + final String user = DB_USER.getString(props); + final RdsUtilities utilities = + RdsUtilities.builder().credentialsProvider(awsCredentialsProvider).region(region).build(); + return utilities.generateAuthenticationToken((builder) -> builder.hostname(hostname).port(port).username(user)); + } catch (final Exception e) { + telemetryContext.setSuccess(false); + telemetryContext.setException(e); + throw e; + } finally { + telemetryContext.closeContext(); + } + } + + private String getCacheKey( + final String user, + final String hostname, + final int port, + final Region region) { + + return String.format("%s:%s:%d:%s", region, hostname, port, user); + } + + public static void clearCache() { + tokenCache.clear(); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginFactory.java new file mode 100644 index 000000000..4236b46f3 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginFactory.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.plugin.federatedauth; + +import java.security.GeneralSecurityException; +import java.util.Properties; +import software.amazon.jdbc.ConnectionPlugin; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; + +public class FederatedAuthPluginFactory implements ConnectionPluginFactory { + + @Override + public ConnectionPlugin getInstance(final PluginService pluginService, final Properties props) { + return new FederatedAuthPlugin(pluginService, getCredentialsProviderFactory(pluginService, props)); + } + + private CredentialsProviderFactory getCredentialsProviderFactory(final PluginService pluginService, + final Properties props) { + final String idpName = FederatedAuthPlugin.IDP_NAME.getString(props); + if (StringUtils.isNullOrEmpty(idpName) || AdfsCredentialsProviderFactory.IDP_NAME.equalsIgnoreCase(idpName)) { + return new AdfsCredentialsProviderFactory( + pluginService, + () -> { + try { + return new HttpClientFactory().getCloseableHttpClient( + FederatedAuthPlugin.HTTP_CLIENT_SOCKET_TIMEOUT.getInteger(props), + FederatedAuthPlugin.HTTP_CLIENT_CONNECT_TIMEOUT.getInteger(props), + FederatedAuthPlugin.SSL_INSECURE.getBoolean(props)); + } catch (GeneralSecurityException e) { + throw new RuntimeException( + Messages.get("FederatedAuthPluginFactory.failedToInitializeHttpClient"), e); + } + }); + } + throw new IllegalArgumentException(Messages.get("FederatedAuthPluginFactory.unsupportedIdp", + new Object[] {idpName})); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/HttpClientFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/HttpClientFactory.java new file mode 100644 index 000000000..db44ff92b --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/HttpClientFactory.java @@ -0,0 +1,71 @@ +/* + * 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.plugin.federatedauth; + +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.LaxRedirectStrategy; + +/** + * Provides a HttpClient so that requests to HTTP API can be made. This is used by the + * {@link software.amazon.jdbc.plugin.federatedauth.AdfsCredentialsProviderFactory} to make HTTP calls to ADFS HTTP + * endpoints that are not available via SDK. + */ +public class HttpClientFactory { + private static final int MAX_REQUEST_RETRIES = 3; + + public CloseableHttpClient getCloseableHttpClient(final int socketTimeoutMs, final int connectionTimeoutMs, + final boolean keySslInsecure) throws GeneralSecurityException { + final RequestConfig rc = RequestConfig.custom() + .setSocketTimeout(socketTimeoutMs) + .setConnectTimeout(connectionTimeoutMs) + .setExpectContinueEnabled(false) + .setCookieSpec(CookieSpecs.STANDARD) + .build(); + + final HttpClientBuilder builder = HttpClients.custom() + .setDefaultRequestConfig(rc) + .setRedirectStrategy(new LaxRedirectStrategy()) + .setRetryHandler(new DefaultHttpRequestRetryHandler(MAX_REQUEST_RETRIES, true)) + .useSystemProperties(); // this is needed for proxy setting using system properties. + + if (keySslInsecure) { + final SSLContext ctx = SSLContext.getInstance("TLSv1.2"); + final TrustManager[] tma = new TrustManager[] {new NonValidatingSSLSocketFactory.NonValidatingTrustManager()}; + ctx.init(null, tma, null); + final SSLSocketFactory factory = ctx.getSocketFactory(); + + final SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory( + factory, + new NoopHostnameVerifier()); + + builder.setSSLSocketFactory(sf); + } + + return builder.build(); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/NonValidatingSSLSocketFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/NonValidatingSSLSocketFactory.java new file mode 100644 index 000000000..100d351b3 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/NonValidatingSSLSocketFactory.java @@ -0,0 +1,98 @@ +/* + * 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.plugin.federatedauth; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Provide a SSLSocketFactory that allows SSL connections to be made without validating the server's + * certificate. This is more convenient for some applications, but is less secure as it allows "man + * in the middle" attacks. + */ +public class NonValidatingSSLSocketFactory extends SSLSocketFactory { + + /** + * We provide a constructor that takes an unused argument solely because the ssl calling code will + * look for this constructor first and then fall back to the no argument constructor, so we avoid + * an exception and additional reflection lookups. + * + * @param arg input argument + * @throws GeneralSecurityException if something goes wrong + */ + public NonValidatingSSLSocketFactory(final String arg) throws GeneralSecurityException { + final SSLContext ctx = SSLContext.getInstance("TLS"); // or "SSL" ? + + ctx.init(null, new TrustManager[]{new NonValidatingTrustManager()}, null); + + factory = ctx.getSocketFactory(); + } + + protected SSLSocketFactory factory; + + public Socket createSocket(final InetAddress host, final int port) throws IOException { + return factory.createSocket(host, port); + } + + public Socket createSocket(final String host, final int port) throws IOException { + return factory.createSocket(host, port); + } + + public Socket createSocket(final String host, final int port, final InetAddress localHost, final int localPort) + throws IOException { + return factory.createSocket(host, port, localHost, localPort); + } + + public Socket createSocket(final InetAddress address, final int port, final InetAddress localAddress, + final int localPort) + throws IOException { + return factory.createSocket(address, port, localAddress, localPort); + } + + public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose) + throws IOException { + return factory.createSocket(socket, host, port, autoClose); + } + + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + public static class NonValidatingTrustManager implements X509TrustManager { + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(final X509Certificate[] certs, final String authType) { + } + + public void checkServerTrusted(final X509Certificate[] certs, final String authType) { + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/SamlCredentialsProviderFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/SamlCredentialsProviderFactory.java new file mode 100644 index 000000000..a2f1081cf --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/federatedauth/SamlCredentialsProviderFactory.java @@ -0,0 +1,60 @@ +/* + * 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.plugin.federatedauth; + +import static software.amazon.jdbc.plugin.federatedauth.FederatedAuthPlugin.IAM_IDP_ARN; +import static software.amazon.jdbc.plugin.federatedauth.FederatedAuthPlugin.IAM_ROLE_ARN; + +import java.sql.SQLException; +import java.util.Properties; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.auth.StsAssumeRoleWithSamlCredentialsProvider; +import software.amazon.awssdk.services.sts.model.AssumeRoleWithSamlRequest; + +public abstract class SamlCredentialsProviderFactory implements CredentialsProviderFactory { + + @Override + public AwsCredentialsProvider getAwsCredentialsProvider(final String host, final Region region, + final @NonNull Properties props) + throws SQLException { + + final String samlAssertion = getSamlAssertion(props); + + final AssumeRoleWithSamlRequest assumeRoleWithSamlRequest = AssumeRoleWithSamlRequest.builder() + .samlAssertion(samlAssertion) + .roleArn(IAM_ROLE_ARN.getString(props)) + .principalArn(IAM_IDP_ARN.getString(props)) + .build(); + + final StsClient stsClient = StsClient.builder() + .credentialsProvider(AnonymousCredentialsProvider.create()) + .region(region) + .build(); + + return StsAssumeRoleWithSamlCredentialsProvider.builder() + .refreshRequest(assumeRoleWithSamlRequest) + .asyncCredentialUpdateEnabled(true) + .stsClient(stsClient) + .build(); + } + + abstract String getSamlAssertion(final @NonNull Properties props) throws SQLException; +} 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 4ea8d6345..1a0756ba0 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 @@ -40,9 +40,6 @@ import software.amazon.jdbc.cleanup.CanReleaseResources; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverSQLException; -import software.amazon.jdbc.states.SessionDirtyFlag; -import software.amazon.jdbc.states.SessionStateHelper; -import software.amazon.jdbc.states.SessionStateTransferCallable; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlState; import software.amazon.jdbc.util.WrapperUtils; @@ -64,9 +61,6 @@ public class ReadWriteSplittingPlugin extends AbstractConnectionPlugin static final String METHOD_SET_READ_ONLY = "Connection.setReadOnly"; static final String METHOD_CLEAR_WARNINGS = "Connection.clearWarnings"; - protected static SessionStateTransferCallable sessionStateTransferCallable; - - private final PluginService pluginService; private final Properties properties; private final String readerSelectorStrategy; @@ -111,14 +105,6 @@ public class ReadWriteSplittingPlugin extends AbstractConnectionPlugin this.readerConnection = readerConnection; } - public static void setSessionStateTransferFunc(SessionStateTransferCallable callable) { - sessionStateTransferCallable = callable; - } - - public static void resetSessionStateTransferFunc() { - sessionStateTransferCallable = null; - } - @Override public Set getSubscribedMethods() { return subscribedMethods; @@ -417,12 +403,11 @@ private void switchCurrentConnectionTo( final Connection newConnection, final HostSpec newConnectionHost) throws SQLException { + final Connection currentConnection = this.pluginService.getCurrentConnection(); if (currentConnection == newConnection) { return; } - - transferSessionStateOnReadWriteSplit(newConnection, newConnectionHost); this.pluginService.setCurrentConnection(newConnection, newConnectionHost); LOGGER.finest(() -> Messages.get( "ReadWriteSplittingPlugin.settingCurrentConnection", @@ -430,48 +415,6 @@ private void switchCurrentConnectionTo( newConnectionHost.getUrl()})); } - /** - * Transfers basic session state from one connection to another, except for the read-only - * status. This method is only called when setReadOnly is being called; the read-only status - * will be updated when the setReadOnly call continues down the plugin chain - * - * @param dest The destination connection to transfer state to - * @param destHostSpec The destination connection {@link HostSpec} - * @throws SQLException if a database access error occurs, this method is called on a closed - * connection, or this method is called during a distributed transaction - */ - protected void transferSessionStateOnReadWriteSplit( - final Connection dest, - final HostSpec destHostSpec) - throws SQLException { - - final Connection src = this.pluginService.getCurrentConnection(); - if (src == null || dest == null) { - return; - } - - EnumSet sessionState = this.pluginService.getCurrentConnectionState(); - - SessionStateTransferCallable callableCopy = sessionStateTransferCallable; - if (callableCopy != null) { - final boolean isHandled = callableCopy.transferSessionState( - sessionState, - src, - this.pluginService.getCurrentHostSpec(), - dest, - destHostSpec); - if (isHandled) { - // Custom function has handled session transfer - return; - } - } - - sessionState = this.pluginService.getCurrentConnectionState(); - sessionState.remove(SessionDirtyFlag.READONLY); // We don't want to change READONLY flag of the connection - final SessionStateHelper helper = new SessionStateHelper(); - helper.transferSessionState(sessionState, src, dest); - } - private synchronized void switchToReaderConnection(final List hosts) throws SQLException { final Connection currentConnection = this.pluginService.getCurrentConnection(); diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPlugin.java new file mode 100644 index 000000000..e94537439 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPlugin.java @@ -0,0 +1,208 @@ +/* + * 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.plugin.strategy.fastestresponse; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.AwsWrapperProperty; +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.PropertyDefinition; +import software.amazon.jdbc.RandomHostSelector; +import software.amazon.jdbc.plugin.AbstractConnectionPlugin; +import software.amazon.jdbc.util.CacheMap; + +public class FastestResponseStrategyPlugin extends AbstractConnectionPlugin { + + private static final Logger LOGGER = + Logger.getLogger(FastestResponseStrategyPlugin.class.getName()); + + public static final String FASTEST_RESPONSE_STRATEGY_NAME = "fastestResponse"; + + private static final Set subscribedMethods = + Collections.unmodifiableSet(new HashSet() { + { + add("notifyNodeListChanged"); + add("acceptsStrategy"); + add("getHostSpecByStrategy"); + } + }); + + public static final AwsWrapperProperty RESPONSE_MEASUREMENT_INTERVAL_MILLIS = + new AwsWrapperProperty( + "responseMeasurementIntervalMs", + "30000", + "Interval in millis between measuring response time to a database node."); + + protected static final CacheMap cachedFastestResponseHostByRole = new CacheMap<>(); + protected static final RandomHostSelector randomHostSelector = new RandomHostSelector(); + + protected final @NonNull PluginService pluginService; + protected final @NonNull Properties properties; + protected final @NonNull HostResponseTimeService hostResponseTimeService; + protected long cacheExpirationNano; + + protected List hosts = new ArrayList<>(); + + static { + PropertyDefinition.registerPluginProperties(FastestResponseStrategyPlugin.class); + PropertyDefinition.registerPluginProperties("frt-"); + } + + public FastestResponseStrategyPlugin(final PluginService pluginService, final @NonNull Properties properties) { + this(pluginService, + properties, + new HostResponseTimeServiceImpl( + pluginService, + properties, + RESPONSE_MEASUREMENT_INTERVAL_MILLIS.getInteger(properties))); + } + + public FastestResponseStrategyPlugin( + final PluginService pluginService, + final @NonNull Properties properties, + final @NonNull HostResponseTimeService hostResponseTimeService) { + + this.pluginService = pluginService; + this.properties = properties; + this.hostResponseTimeService = hostResponseTimeService; + this.cacheExpirationNano = TimeUnit.MILLISECONDS.toNanos( + RESPONSE_MEASUREMENT_INTERVAL_MILLIS.getInteger(this.properties)); + } + + @Override + public Set getSubscribedMethods() { + return subscribedMethods; + } + + @Override + public Connection connect( + final String driverProtocol, + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable connectFunc) + throws SQLException { + + Connection conn = connectFunc.call(); + if (isInitialConnection) { + this.hostResponseTimeService.setHosts(this.pluginService.getHosts()); + } + return conn; + } + + @Override + public Connection forceConnect( + final String driverProtocol, + final HostSpec hostSpec, + final Properties props, + final boolean isInitialConnection, + final JdbcCallable forceConnectFunc) + throws SQLException { + + Connection conn = forceConnectFunc.call(); + if (isInitialConnection) { + this.hostResponseTimeService.setHosts(this.pluginService.getHosts()); + } + return conn; + } + + @Override + public boolean acceptsStrategy(HostRole role, String strategy) { + return FASTEST_RESPONSE_STRATEGY_NAME.equalsIgnoreCase(strategy); + } + + @Override + public HostSpec getHostSpecByStrategy(final HostRole role, final String strategy) + throws SQLException, UnsupportedOperationException { + + if (!acceptsStrategy(role, strategy)) { + return null; + } + + // The cache holds a host with the fastest response time. + // If cache doesn't have a host for a role, it's necessary to find the fastest node in the topology. + final HostSpec fastestResponseHost = cachedFastestResponseHostByRole.get(role.name()); + + if (fastestResponseHost != null) { + // Found a fastest host. Let find it in the the latest topology. + HostSpec foundHostSpec = this.pluginService.getHosts().stream() + .filter(x -> x.equals(fastestResponseHost)) + .findAny() + .orElse(null); + + if (foundHostSpec != null) { + // Found a host in the topology. + return foundHostSpec; + } + + // It seems that the fastest cached host isn't in the latest topology. + // Let's ignore cached results and find the fastest host. + } + + // Cached result isn't available. Need to find the fastest response time host. + + final HostSpec calculatedFastestResponseHost = this.pluginService.getHosts().stream() + .filter(x -> role.equals(x.getRole())) + .map(x -> new ResponseTimeTuple(x, this.hostResponseTimeService.getResponseTime(x))) + .sorted(Comparator.comparingInt(x -> x.responseTime)) + .map(x -> x.hostSpec) + .findFirst() + .orElse(null); + + if (calculatedFastestResponseHost == null) { + // Unable to identify the fastest response host. + // As a last resort, let's use a random host selector. + return randomHostSelector.getHost(this.hosts, role, properties); + } + + cachedFastestResponseHostByRole.put(role.name(), calculatedFastestResponseHost, this.cacheExpirationNano); + + return calculatedFastestResponseHost; + } + + @Override + public void notifyNodeListChanged(final Map> changes) { + this.hosts = this.pluginService.getHosts(); + this.hostResponseTimeService.setHosts(this.hosts); + } + + private static class ResponseTimeTuple { + public HostSpec hostSpec; + public int responseTime; + + public ResponseTimeTuple(final HostSpec hostSpec, int responseTime) { + this.hostSpec = hostSpec; + this.responseTime = responseTime; + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPluginFactory.java new file mode 100644 index 000000000..87a1d766b --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/FastestResponseStrategyPluginFactory.java @@ -0,0 +1,30 @@ +/* + * 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.plugin.strategy.fastestresponse; + +import java.util.Properties; +import software.amazon.jdbc.ConnectionPlugin; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.PluginService; + +public class FastestResponseStrategyPluginFactory implements ConnectionPluginFactory { + + @Override + public ConnectionPlugin getInstance(final PluginService pluginService, final Properties props) { + return new FastestResponseStrategyPlugin(pluginService, props); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeService.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeService.java new file mode 100644 index 000000000..2350c4b47 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeService.java @@ -0,0 +1,39 @@ +/* + * 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.plugin.strategy.fastestresponse; + +import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.HostSpec; + +public interface HostResponseTimeService { + + /** + * Return a response time in milliseconds to the host. + * Return Integer.MAX_VALUE if response time is not available. + * + * @param hostSpec the host details + * @return response time in milliseconds for a desired host. It should return Integer.MAX_VALUE if + * response time couldn't be measured. + */ + int getResponseTime(final HostSpec hostSpec); + + /** + * Provides an updated host list to a service. + */ + void setHosts(final @NonNull List hosts); +} 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 new file mode 100644 index 000000000..782662838 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java @@ -0,0 +1,112 @@ +/* + * 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.plugin.strategy.fastestresponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Set; +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 software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.SlidingExpirationCacheWithCleanupThread; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; +import software.amazon.jdbc.util.telemetry.TelemetryGauge; + +public class HostResponseTimeServiceImpl implements HostResponseTimeService { + + private static final Logger LOGGER = + Logger.getLogger(HostResponseTimeServiceImpl.class.getName()); + + protected static final long CACHE_EXPIRATION_NANO = TimeUnit.MINUTES.toNanos(10); + protected static final long CACHE_CLEANUP_NANO = TimeUnit.MINUTES.toNanos(1); + + protected static final SlidingExpirationCacheWithCleanupThread monitoringNodes + = new SlidingExpirationCacheWithCleanupThread<>( + (monitor) -> true, + (monitor) -> { + try { + monitor.close(); + } catch (Exception ex) { + // ignore + } + }, + CACHE_CLEANUP_NANO); + protected static final ReentrantLock cacheLock = new ReentrantLock(); + + protected int intervalMs; + + protected List hosts = new ArrayList<>(); + + protected final @NonNull PluginService pluginService; + + protected final @NonNull Properties props; + + protected final TelemetryFactory telemetryFactory; + private final TelemetryGauge nodeCountGauge; + + public HostResponseTimeServiceImpl( + final @NonNull PluginService pluginService, + final @NonNull Properties props, + int intervalMs) { + + this.pluginService = pluginService; + this.props = props; + this.intervalMs = intervalMs; + this.telemetryFactory = this.pluginService.getTelemetryFactory(); + this.nodeCountGauge = telemetryFactory.createGauge("frt.nodes.count", + () -> (long) monitoringNodes.size()); + + monitoringNodes.setCleanupIntervalNanos(CACHE_CLEANUP_NANO); + } + + @Override + public int getResponseTime(HostSpec hostSpec) { + final NodeResponseTimeMonitor monitor = monitoringNodes.get(hostSpec.getUrl(), CACHE_EXPIRATION_NANO); + if (monitor == null) { + return Integer.MAX_VALUE; + } + + return monitor.getResponseTime(); + } + + @Override + public void setHosts(final @NonNull List hosts) { + Set oldHosts = this.hosts.stream().map(HostSpec::getUrl).collect(Collectors.toSet()); + this.hosts = hosts; + + // Going through all hosts in the topology and trying to find new ones. + this.hosts.stream() + // hostSpec is not in the set of hosts that already being monitored + .filter(hostSpec -> !oldHosts.contains(hostSpec.getUrl())) + .forEach(hostSpec -> { + cacheLock.lock(); + try { + monitoringNodes.computeIfAbsent( + hostSpec.getUrl(), + (key) -> new NodeResponseTimeMonitor(this.pluginService, hostSpec, this.props, this.intervalMs), + CACHE_EXPIRATION_NANO); + } finally { + cacheLock.unlock(); + } + }); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/NodeResponseTimeMonitor.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/NodeResponseTimeMonitor.java new file mode 100644 index 000000000..602f8d075 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/NodeResponseTimeMonitor.java @@ -0,0 +1,234 @@ +/* + * 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.plugin.strategy.fastestresponse; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.PropertyUtils; +import software.amazon.jdbc.util.StringUtils; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; +import software.amazon.jdbc.util.telemetry.TelemetryGauge; +import software.amazon.jdbc.util.telemetry.TelemetryTraceLevel; + +public class NodeResponseTimeMonitor implements AutoCloseable, Runnable { + + private static final Logger LOGGER = + Logger.getLogger(NodeResponseTimeMonitor.class.getName()); + + private static final String MONITORING_PROPERTY_PREFIX = "frt-"; + private static final int NUM_OF_MEASURES = 5; + + private final int intervalMs; + private final @NonNull HostSpec hostSpec; + + private final AtomicBoolean stopped = new AtomicBoolean(false); + private final AtomicInteger responseTime = new AtomicInteger(Integer.MAX_VALUE); + private final AtomicLong checkTimestamp = new AtomicLong(this.getCurrentTime()); + + private final @NonNull Properties props; + private final @NonNull PluginService pluginService; + + private final TelemetryFactory telemetryFactory; + private final TelemetryGauge responseTimeMsGauge; + + + private Connection monitoringConn = null; + + private final ExecutorService threadPool = Executors.newFixedThreadPool(1, runnableTarget -> { + final Thread monitoringThread = new Thread(runnableTarget); + monitoringThread.setDaemon(true); + return monitoringThread; + }); + + public NodeResponseTimeMonitor( + final @NonNull PluginService pluginService, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props, + int intervalMs) { + + this.pluginService = pluginService; + this.hostSpec = hostSpec; + this.props = props; + this.intervalMs = intervalMs; + this.telemetryFactory = this.pluginService.getTelemetryFactory(); + + final String nodeId = StringUtils.isNullOrEmpty(this.hostSpec.getHostId()) + ? this.hostSpec.getHost() + : this.hostSpec.getHostId(); + + // Report current response time (in milliseconds) to telemetry engine. + // Report -1 if response time couldn't be measured. + this.responseTimeMsGauge = telemetryFactory.createGauge( + String.format("frt.response.time.%s", nodeId), + () -> this.responseTime.get() == Integer.MAX_VALUE ? -1 : (long) this.responseTime.get()); + + this.threadPool.submit(this); + this.threadPool.shutdown(); // No more task are accepted by pool. + } + + // Return node response time in milliseconds. + public int getResponseTime() { + return this.responseTime.get(); + } + + public long getCheckTimestamp() { + return this.checkTimestamp.get(); + } + + public HostSpec getHostSpec() { + return this.hostSpec; + } + + @Override + public void close() throws Exception { + this.stopped.set(true); + + // Waiting for 5s gives a thread enough time to exit monitoring loop and close database connection. + if (!this.threadPool.awaitTermination(5, TimeUnit.SECONDS)) { + this.threadPool.shutdownNow(); + } + LOGGER.finest(() -> Messages.get( + "NodeResponseTimeMonitor.stopped", + new Object[] {this.hostSpec.getHost()})); + } + + // The method is for testing purposes. + protected long getCurrentTime() { + return System.nanoTime(); + } + + @Override + public void run() { + TelemetryContext telemetryContext = telemetryFactory.openTelemetryContext( + "node response time thread", TelemetryTraceLevel.TOP_LEVEL); + telemetryContext.setAttribute("url", hostSpec.getUrl()); + + try { + while (!this.stopped.get()) { + this.openConnection(); + + if (this.monitoringConn != null) { + + long responseTimeSum = 0; + int count = 0; + for (int i = 0; i < NUM_OF_MEASURES; i++) { + if (this.stopped.get()) { + break; + } + long startTime = this.getCurrentTime(); + if (this.pluginService.getTargetDriverDialect().ping(this.monitoringConn)) { + long responseTime = this.getCurrentTime() - startTime; + responseTimeSum += responseTime; + count++; + } + } + + if (count > 0) { + this.responseTime.set((int) TimeUnit.NANOSECONDS.toMillis(responseTimeSum / count)); + } else { + this.responseTime.set(Integer.MAX_VALUE); + } + this.checkTimestamp.set(this.getCurrentTime()); + + LOGGER.finest(() -> Messages.get( + "NodeResponseTimeMonitor.responseTime", + new Object[] {this.hostSpec.getHost(), this.responseTime.get()})); + } + + TimeUnit.MILLISECONDS.sleep(this.intervalMs); + } + } catch (final InterruptedException intEx) { + // exit thread + LOGGER.finest( + () -> Messages.get( + "NodeResponseTimeMonitor.interruptedExceptionDuringMonitoring", + new Object[] {this.hostSpec.getHost()})); + } catch (final Exception ex) { + // this should not be reached; log and exit thread + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log( + Level.FINEST, + Messages.get( + "NodeResponseTimeMonitor.exceptionDuringMonitoringStop", + new Object[]{this.hostSpec.getHost()}), + ex); // We want to print full trace stack of the exception. + } + } finally { + this.stopped.set(true); + if (this.monitoringConn != null) { + try { + this.monitoringConn.close(); + } catch (final SQLException ex) { + // ignore + } + } + if (telemetryContext != null) { + telemetryContext.closeContext(); + } + } + } + + private void openConnection() { + try { + if (this.monitoringConn == null || this.monitoringConn.isClosed()) { + // open a new connection + final Properties monitoringConnProperties = PropertyUtils.copyProperties(this.props); + + this.props.stringPropertyNames().stream() + .filter(p -> p.startsWith(MONITORING_PROPERTY_PREFIX)) + .forEach( + p -> { + monitoringConnProperties.put( + p.substring(MONITORING_PROPERTY_PREFIX.length()), + this.props.getProperty(p)); + monitoringConnProperties.remove(p); + }); + + LOGGER.finest(() -> Messages.get( + "NodeResponseTimeMonitor.openingConnection", + new Object[] {this.hostSpec.getUrl()})); + this.monitoringConn = this.pluginService.forceConnect(this.hostSpec, monitoringConnProperties); + LOGGER.finest(() -> Messages.get( + "NodeResponseTimeMonitor.openedConnection", + new Object[] {this.monitoringConn})); + } + } catch (SQLException ex) { + if (this.monitoringConn != null) { + try { + this.monitoringConn.close(); + } catch (Exception e) { + // ignore + } + this.monitoringConn = null; + } + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfile.java b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfile.java new file mode 100644 index 000000000..c8bf2af67 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfile.java @@ -0,0 +1,193 @@ +/* + * 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.profile; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.ConnectionProvider; +import software.amazon.jdbc.authentication.AwsCredentialsProviderHandler; +import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.exceptions.ExceptionHandler; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; + +public class ConfigurationProfile { + + private final @NonNull String name; + private final @Nullable List> pluginFactories; + private final @Nullable Properties properties; + private @Nullable Supplier dialectSupplier; + private @Nullable Supplier targetDriverDialectSupplier; + private @Nullable Supplier exceptionHandlerSupplier; + private @Nullable Supplier awsCredentialsProviderHandlerSupplier; + private @Nullable Supplier connectionProviderSupplier; + + private @Nullable Dialect dialect; + private @Nullable TargetDriverDialect targetDriverDialect; + private @Nullable ExceptionHandler exceptionHandler; + private @Nullable AwsCredentialsProviderHandler awsCredentialsProviderHandler; + private @Nullable ConnectionProvider connectionProvider; + + private final ReentrantLock lock = new ReentrantLock(); + + ConfigurationProfile(final @NonNull String name, + @Nullable List> pluginFactories, + @Nullable Properties properties, + @Nullable Supplier dialectSupplier, + @Nullable Supplier targetDriverDialectSupplier, + @Nullable Supplier exceptionHandlerSupplier, + @Nullable Supplier connectionProviderSupplier, + @Nullable Supplier credentialsProviderHandlerSupplier) { + + this.name = name; + this.pluginFactories = pluginFactories; + this.properties = properties; + this.dialectSupplier = dialectSupplier; + this.targetDriverDialectSupplier = targetDriverDialectSupplier; + this.exceptionHandlerSupplier = exceptionHandlerSupplier; + this.connectionProviderSupplier = connectionProviderSupplier; + this.awsCredentialsProviderHandlerSupplier = credentialsProviderHandlerSupplier; + } + + ConfigurationProfile(final @NonNull String name, + @Nullable List> pluginFactories, + @Nullable Properties properties, + @Nullable Dialect dialect, + @Nullable TargetDriverDialect targetDriverDialect, + @Nullable ExceptionHandler exceptionHandler, + @Nullable ConnectionProvider connectionProvider, + @Nullable AwsCredentialsProviderHandler credentialsProviderHandler) { + + this.name = name; + this.pluginFactories = pluginFactories; + this.properties = properties; + this.dialect = dialect; + this.targetDriverDialect = targetDriverDialect; + this.exceptionHandler = exceptionHandler; + this.connectionProvider = connectionProvider; + this.awsCredentialsProviderHandler = credentialsProviderHandler; + } + + public @NonNull String getName() { + return this.name; + } + + public @Nullable Properties getProperties() { + return this.properties; + } + + public @Nullable List> getPluginFactories() { + return this.pluginFactories; + } + + public @Nullable Dialect getDialect() { + if (this.dialect != null) { + return this.dialect; + } + if (this.dialectSupplier == null) { + return null; + } + + this.lock.lock(); + try { + this.dialect = this.dialectSupplier.get(); + return this.dialect; + } finally { + this.lock.unlock(); + } + } + + public @Nullable TargetDriverDialect getTargetDriverDialect() { + if (this.targetDriverDialect != null) { + return this.targetDriverDialect; + } + if (this.targetDriverDialectSupplier == null) { + return null; + } + try { + this.lock.lock(); + if (this.targetDriverDialect != null) { + return this.targetDriverDialect; + } + this.targetDriverDialect = this.targetDriverDialectSupplier.get(); + return this.targetDriverDialect; + } finally { + this.lock.unlock(); + } + } + + public @Nullable ExceptionHandler getExceptionHandler() { + if (this.exceptionHandler != null) { + return this.exceptionHandler; + } + if (this.exceptionHandlerSupplier == null) { + return null; + } + try { + this.lock.lock(); + if (this.exceptionHandler != null) { + return this.exceptionHandler; + } + this.exceptionHandler = this.exceptionHandlerSupplier.get(); + return this.exceptionHandler; + } finally { + this.lock.unlock(); + } + } + + public @Nullable ConnectionProvider getConnectionProvider() { + if (this.connectionProvider != null) { + return this.connectionProvider; + } + if (this.connectionProviderSupplier == null) { + return null; + } + try { + this.lock.lock(); + if (this.connectionProvider != null) { + return this.connectionProvider; + } + this.connectionProvider = this.connectionProviderSupplier.get(); + return this.connectionProvider; + } finally { + this.lock.unlock(); + } + } + + public @Nullable AwsCredentialsProviderHandler getAwsCredentialsProviderHandler() { + if (this.awsCredentialsProviderHandler != null) { + return this.awsCredentialsProviderHandler; + } + if (this.awsCredentialsProviderHandlerSupplier == null) { + return null; + } + try { + this.lock.lock(); + if (this.awsCredentialsProviderHandler != null) { + return this.awsCredentialsProviderHandler; + } + this.awsCredentialsProviderHandler = this.awsCredentialsProviderHandlerSupplier.get(); + return this.awsCredentialsProviderHandler; + } finally { + this.lock.unlock(); + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfileBuilder.java b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfileBuilder.java new file mode 100644 index 000000000..43d886e98 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfileBuilder.java @@ -0,0 +1,136 @@ +/* + * 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.profile; + +import java.util.List; +import java.util.Properties; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.ConnectionProvider; +import software.amazon.jdbc.authentication.AwsCredentialsProviderHandler; +import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.exceptions.ExceptionHandler; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; + +public class ConfigurationProfileBuilder { + + private String name; + private @Nullable List> pluginFactories; + private @Nullable Properties properties; + private @Nullable Dialect dialect; + private @Nullable TargetDriverDialect targetDriverDialect; + private @Nullable ExceptionHandler exceptionHandler; + private @Nullable AwsCredentialsProviderHandler awsCredentialsProviderHandler; + private @Nullable ConnectionProvider connectionProvider; + + private ConfigurationProfileBuilder() { } + + public static ConfigurationProfileBuilder get() { + return new ConfigurationProfileBuilder(); + } + + public ConfigurationProfileBuilder withName(final @NonNull String name) { + this.name = name; + return this; + } + + public ConfigurationProfileBuilder withProperties(final @Nullable Properties properties) { + this.properties = properties; + return this; + } + + public ConfigurationProfileBuilder withPluginFactories( + final @Nullable List> pluginFactories) { + this.pluginFactories = pluginFactories; + return this; + } + + public ConfigurationProfileBuilder withDialect(final @Nullable Dialect dialect) { + this.dialect = dialect; + return this; + } + + public ConfigurationProfileBuilder withTargetDriverDialect( + final @Nullable TargetDriverDialect targetDriverDialect) { + this.targetDriverDialect = targetDriverDialect; + return this; + } + + public ConfigurationProfileBuilder withExceptionHandler( + final @Nullable ExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + return this; + } + + public ConfigurationProfileBuilder withConnectionProvider( + final @Nullable ConnectionProvider connectionProvider) { + this.connectionProvider = connectionProvider; + return this; + } + + public ConfigurationProfileBuilder withAwsCredentialsProviderHandler( + final @Nullable AwsCredentialsProviderHandler awsCredentialsProviderHandler) { + this.awsCredentialsProviderHandler = awsCredentialsProviderHandler; + return this; + } + + public ConfigurationProfileBuilder from(final @NonNull String presetProfileName) { + final ConfigurationProfile configurationProfile = + DriverConfigurationProfiles.getProfileConfiguration(presetProfileName); + + if (configurationProfile == null) { + throw new RuntimeException(Messages.get( + "Driver.configurationProfileNotFound", + new Object[] {presetProfileName})); + } + + this.pluginFactories = configurationProfile.getPluginFactories(); + this.properties = configurationProfile.getProperties(); + this.dialect = configurationProfile.getDialect(); + this.targetDriverDialect = configurationProfile.getTargetDriverDialect(); + this.exceptionHandler = configurationProfile.getExceptionHandler(); + this.connectionProvider = configurationProfile.getConnectionProvider(); + this.awsCredentialsProviderHandler = configurationProfile.getAwsCredentialsProviderHandler(); + + return this; + } + + public ConfigurationProfile build() { + if (StringUtils.isNullOrEmpty(this.name)) { + throw new RuntimeException("Profile name is required."); + } + if (ConfigurationProfilePresetCodes.isKnownPreset(this.name)) { + throw new RuntimeException("Can't add or update a built-in preset configuration profile."); + } + + return new ConfigurationProfile(this.name, + this.pluginFactories, + this.properties, + this.dialect, + this.targetDriverDialect, + this.exceptionHandler, + this.connectionProvider, + this.awsCredentialsProviderHandler); + } + + public void buildAndSet() { + DriverConfigurationProfiles.addOrReplaceProfile(this.name, this.build()); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfilePresetCodes.java b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfilePresetCodes.java new file mode 100644 index 000000000..728e0b8a6 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/profile/ConfigurationProfilePresetCodes.java @@ -0,0 +1,74 @@ +/* + * 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.profile; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class ConfigurationProfilePresetCodes { + + // Presets family A, B, C - no connection pool + // Presets family D, E ,F - internal connection pool + // Presets family G, H, I - external connection pool + + public static final String A0 = "A0"; // Normal + public static final String A1 = "A1"; // Easy + public static final String A2 = "A2"; // Aggressive + public static final String B = "B"; // Normal + + public static final String C0 = "C0"; // Normal + public static final String C1 = "C1"; // Aggressive + + public static final String D0 = "D0"; // Normal + public static final String D1 = "D1"; // Easy + + public static final String E = "E"; // Normal + + public static final String F0 = "F0"; // Normal + public static final String F1 = "F1"; // Aggressive + + public static final String G0 = "G0"; // Normal + public static final String G1 = "G1"; // Easy + + public static final String H = "H"; // Normal + + public static final String I0 = "I0"; // Normal + public static final String I1 = "I1"; // Aggressive + + private static final Set KNOWN_PRESETS = ConcurrentHashMap.newKeySet(); + + static { + registerProperties(String.class); + } + + public static boolean isKnownPreset(final @NonNull String presetName) { + return KNOWN_PRESETS.contains(presetName); + } + + private static void registerProperties(final Class ownerClass) { + Arrays.stream(ownerClass.getDeclaredFields()) + .filter( + f -> + f.getType() == ownerClass + && Modifier.isPublic(f.getModifiers()) + && Modifier.isStatic(f.getModifiers())) + .forEach(f -> KNOWN_PRESETS.add(f.getName())); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/profile/DriverConfigurationProfiles.java b/wrapper/src/main/java/software/amazon/jdbc/profile/DriverConfigurationProfiles.java index 996e2b8a3..bbe7704aa 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/profile/DriverConfigurationProfiles.java +++ b/wrapper/src/main/java/software/amazon/jdbc/profile/DriverConfigurationProfiles.java @@ -16,37 +16,508 @@ package software.amazon.jdbc.profile; -import java.util.List; +import com.zaxxer.hikari.HikariConfig; +import java.util.Arrays; +import java.util.Collections; import java.util.Map; +import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.NonNull; -import software.amazon.jdbc.ConnectionPluginFactory; +import software.amazon.jdbc.HikariPooledConnectionProvider; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.plugin.AuroraConnectionTrackerPluginFactory; +import software.amazon.jdbc.plugin.AuroraHostListConnectionPluginFactory; +import software.amazon.jdbc.plugin.AuroraInitialConnectionStrategyPluginFactory; +import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin; +import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPluginFactory; +import software.amazon.jdbc.plugin.failover.FailoverConnectionPluginFactory; +import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPluginFactory; +import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsPluginFactory; public class DriverConfigurationProfiles { - private static final Map>> profiles = + private static final Map presets; + + private static final Map activeProfiles = new ConcurrentHashMap<>(); + private static final String MONITORING_CONNECTION_PREFIX = "monitoring-"; + + static { + presets = getConfigurationProfilePresets(); + } + public static void clear() { - profiles.clear(); + activeProfiles.clear(); } public static void addOrReplaceProfile( @NonNull final String profileName, - @NonNull final List> pluginFactories) { - profiles.put(profileName, pluginFactories); + @NonNull final ConfigurationProfile configurationProfile) { + activeProfiles.put(profileName, configurationProfile); } public static void remove(@NonNull final String profileName) { - profiles.remove(profileName); + activeProfiles.remove(profileName); } public static boolean contains(@NonNull final String profileName) { - return profiles.containsKey(profileName); + return activeProfiles.containsKey(profileName); + } + + public static ConfigurationProfile getProfileConfiguration(@NonNull final String profileName) { + ConfigurationProfile profile = activeProfiles.get(profileName); + + if (profile != null) { + return profile; + } + return presets.get(profileName); + } + + private static Map getConfigurationProfilePresets() { + Map presets = new ConcurrentHashMap<>(); + + presets.put(ConfigurationProfilePresetCodes.A0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A0, + Collections.emptyList(), // empty list is important here! it shouldn't be a null. + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.A1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A1, + Collections.emptyList(), // empty list is important here! it shouldn't be a null. + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "30000", + PropertyDefinition.SOCKET_TIMEOUT.name, "30000", + PropertyDefinition.LOGIN_TIMEOUT.name, "30000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.A2, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.A2, + Collections.emptyList(), // empty list is important here! it shouldn't be a null. + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "3000", + PropertyDefinition.SOCKET_TIMEOUT.name, "3000", + PropertyDefinition.LOGIN_TIMEOUT.name, "3000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.B, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.B, + Collections.emptyList(), // empty list is important here! it shouldn't be a null. + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "true"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.C0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.C0, + Collections.singletonList(HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "60000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "5", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "15000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.C1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.C1, + Collections.singletonList(HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "30000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "3", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "3000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.D0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.D0, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraInitialConnectionStrategyPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + ReadWriteSplittingPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + null, + null, + null, + () -> new HikariPooledConnectionProvider( + (HostSpec hostSpec, Properties originalProps) -> { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + // holds few extra connections in case of sudden traffic peak + config.setMinimumIdle(2); + // close idle connection in 15min; helps to get back to normal pool size after load peak + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + // verify pool configuration and creates no connections during initialization phase + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(10)); + // validate idle connections at least every 3 min + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + // allows to quickly validate connection in the pool and move on to another connection if needed + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + return config; + }, + null + ), + null)); + + presets.put(ConfigurationProfilePresetCodes.D1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.D1, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraInitialConnectionStrategyPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + ReadWriteSplittingPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "30000", + PropertyDefinition.SOCKET_TIMEOUT.name, "30000", + PropertyDefinition.LOGIN_TIMEOUT.name, "30000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + null, + null, + null, + () -> new HikariPooledConnectionProvider( + (HostSpec hostSpec, Properties originalProps) -> { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + // holds few extra connections in case of sudden traffic peak + config.setMinimumIdle(2); + // close idle connection in 15min; helps to get back to normal pool size after load peak + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + // verify pool configuration and creates no connections during initialization phase + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(10)); + // validate idle connections at least every 3 min + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + // allows to quickly validate connection in the pool and move on to another connection if needed + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + return config; + }, + null + ), + null)); + + presets.put(ConfigurationProfilePresetCodes.E, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.E, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraInitialConnectionStrategyPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + ReadWriteSplittingPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "true"), + null, + null, + null, + () -> new HikariPooledConnectionProvider( + (HostSpec hostSpec, Properties originalProps) -> { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + // holds few extra connections in case of sudden traffic peak + config.setMinimumIdle(2); + // close idle connection in 15min; helps to get back to normal pool size after load peak + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + // verify pool configuration and creates no connections during initialization phase + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(10)); + // validate idle connections at least every 3 min + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + // allows to quickly validate connection in the pool and move on to another connection if needed + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + return config; + }, + null + ), + null)); + + presets.put(ConfigurationProfilePresetCodes.F0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.F0, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraInitialConnectionStrategyPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + ReadWriteSplittingPluginFactory.class, + FailoverConnectionPluginFactory.class, + HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "60000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "5", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "15000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + null, + null, + null, + () -> new HikariPooledConnectionProvider( + (HostSpec hostSpec, Properties originalProps) -> { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + // holds few extra connections in case of sudden traffic peak + config.setMinimumIdle(2); + // close idle connection in 15min; helps to get back to normal pool size after load peak + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + // verify pool configuration and creates no connections during initialization phase + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(10)); + // validate idle connections at least every 3 min + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + // allows to quickly validate connection in the pool and move on to another connection if needed + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + return config; + }, + null + ), + null)); + + presets.put(ConfigurationProfilePresetCodes.F1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.F1, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraInitialConnectionStrategyPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + ReadWriteSplittingPluginFactory.class, + FailoverConnectionPluginFactory.class, + HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "30000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "3", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "3000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + null, + null, + null, + () -> new HikariPooledConnectionProvider( + (HostSpec hostSpec, Properties originalProps) -> { + final HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + // holds few extra connections in case of sudden traffic peak + config.setMinimumIdle(2); + // close idle connection in 15min; helps to get back to normal pool size after load peak + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + // verify pool configuration and creates no connections during initialization phase + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(10)); + // validate idle connections at least every 3 min + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + // allows to quickly validate connection in the pool and move on to another connection if needed + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + return config; + }, + null + ), + null)); + + presets.put(ConfigurationProfilePresetCodes.G0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.G0, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + AuroraStaleDnsPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.G1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.G1, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + AuroraStaleDnsPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "30000", + PropertyDefinition.SOCKET_TIMEOUT.name, "30000", + PropertyDefinition.LOGIN_TIMEOUT.name, "30000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.H, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.H, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + AuroraStaleDnsPluginFactory.class, + FailoverConnectionPluginFactory.class), + getProperties( + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "true"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.I0, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.I0, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + AuroraStaleDnsPluginFactory.class, + FailoverConnectionPluginFactory.class, + HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "60000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "5", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "15000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + presets.put(ConfigurationProfilePresetCodes.I1, + new ConfigurationProfile( + ConfigurationProfilePresetCodes.I1, + Arrays.asList( + AuroraHostListConnectionPluginFactory.class, + AuroraConnectionTrackerPluginFactory.class, + AuroraStaleDnsPluginFactory.class, + FailoverConnectionPluginFactory.class, + HostMonitoringConnectionPluginFactory.class), + getProperties( + HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME.name, "30000", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT.name, "3", + HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL.name, "5000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, "3000", + MONITORING_CONNECTION_PREFIX + PropertyDefinition.LOGIN_TIMEOUT.name, "3000", + PropertyDefinition.CONNECT_TIMEOUT.name, "10000", + PropertyDefinition.SOCKET_TIMEOUT.name, "0", + PropertyDefinition.LOGIN_TIMEOUT.name, "10000", + PropertyDefinition.TCP_KEEP_ALIVE.name, "false"), + (Dialect) null, + null, + null, + null, + null)); + + return presets; } - public static List> getPluginFactories( - @NonNull final String profileName) { - return profiles.get(profileName); + private static Properties getProperties(String... args) { + if (args == null) { + return null; + } + + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Properties should be passed by pairs: property name and property value."); + } + + final Properties props = new Properties(); + + for (int i = 0; i < args.length; i += 2) { + props.put(args[i], args[i + 1]); + } + + return props; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionDirtyFlag.java b/wrapper/src/main/java/software/amazon/jdbc/states/ResetSessionStateOnCloseCallable.java similarity index 69% rename from wrapper/src/main/java/software/amazon/jdbc/states/SessionDirtyFlag.java rename to wrapper/src/main/java/software/amazon/jdbc/states/ResetSessionStateOnCloseCallable.java index 985da0204..b15518a0d 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/states/SessionDirtyFlag.java +++ b/wrapper/src/main/java/software/amazon/jdbc/states/ResetSessionStateOnCloseCallable.java @@ -16,18 +16,11 @@ package software.amazon.jdbc.states; +import java.sql.Connection; +import java.sql.SQLException; +import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.EnumSet; - -public enum SessionDirtyFlag { - READONLY, - AUTO_COMMIT, - TRANSACTION_ISOLATION, - CATALOG, - NETWORK_TIMEOUT, - SCHEMA, - TYPE_MAP, - HOLDABILITY; - - public static final EnumSet ALL = EnumSet.allOf(SessionDirtyFlag.class); +public interface ResetSessionStateOnCloseCallable { + boolean apply(final @NonNull SessionState sessionState, final @NonNull Connection connectionToClose) + throws SQLException; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/RestoreSessionStateCallable.java b/wrapper/src/main/java/software/amazon/jdbc/states/RestoreSessionStateCallable.java deleted file mode 100644 index b1e3a236e..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/states/RestoreSessionStateCallable.java +++ /dev/null @@ -1,42 +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.states; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.EnumSet; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -public interface RestoreSessionStateCallable { - /** - * Restores partial session state from saved values to a connection. - * - * @param sessionState Session state flags for from-connection - * @param dest The destination connection to transfer state to - * @param readOnly ReadOnly flag to set to - * @param autoCommit AutoCommit flag to set to - * @return true, if session state is restored successful and no default logic should be executed after. - * False, if default logic should be executed. - */ - boolean restoreSessionState( - final @NonNull EnumSet sessionState, - final @NonNull Connection dest, - final @Nullable Boolean readOnly, - final @Nullable Boolean autoCommit) - throws SQLException; -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionState.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionState.java new file mode 100644 index 000000000..c4306c233 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/states/SessionState.java @@ -0,0 +1,52 @@ +/* + * 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.states; + +import java.util.HashMap; +import java.util.Map; + +public class SessionState { + public SessionStateField autoCommit = new SessionStateField<>(); + public SessionStateField readOnly = new SessionStateField<>(); + public SessionStateField catalog = new SessionStateField<>(); + public SessionStateField schema = new SessionStateField<>(); + public SessionStateField holdability = new SessionStateField<>(); + public SessionStateField networkTimeout = new SessionStateField<>(); + public SessionStateField transactionIsolation = new SessionStateField<>(); + public SessionStateField>> typeMap = new SessionStateField<>(); + + public SessionState copy() { + final SessionState newSessionState = new SessionState(); + newSessionState.autoCommit = this.autoCommit.copy(); + newSessionState.readOnly = this.readOnly.copy(); + newSessionState.catalog = this.catalog.copy(); + newSessionState.schema = this.schema.copy(); + newSessionState.holdability = this.holdability.copy(); + newSessionState.networkTimeout = this.networkTimeout.copy(); + newSessionState.transactionIsolation = this.transactionIsolation.copy(); + + // typeMap requires a special care since it uses map, and it needs to be properly cloned. + if (this.typeMap.getValue().isPresent()) { + newSessionState.typeMap.setValue(new HashMap<>(this.typeMap.getValue().get())); + } + if (this.typeMap.getPristineValue().isPresent()) { + newSessionState.typeMap.setPristineValue(new HashMap<>(this.typeMap.getPristineValue().get())); + } + + return newSessionState; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateField.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateField.java new file mode 100644 index 000000000..3824891b3 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateField.java @@ -0,0 +1,92 @@ +/* + * 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.states; + +import java.util.Optional; + +public class SessionStateField { + private Optional value = Optional.empty(); + private Optional pristineValue = Optional.empty(); + + public SessionStateField copy() { + final SessionStateField newField = new SessionStateField<>(); + if (this.value.isPresent()) { + newField.setValue(this.value.get()); + } + if (this.pristineValue.isPresent()) { + newField.setPristineValue(this.pristineValue.get()); + } + return newField; + } + + public Optional getValue() { + return this.value; + } + + public Optional getPristineValue() { + return this.pristineValue; + } + + public void setValue(final T value) { + this.value = Optional.of(value); + } + + public void setPristineValue(final T value) { + this.pristineValue = Optional.of(value); + } + + public void resetValue() { + this.value = Optional.empty(); + } + + public void resetPristineValue() { + this.pristineValue = Optional.empty(); + } + + public void reset() { + this.resetValue(); + this.resetPristineValue(); + } + + public boolean isPristine() { + // the value has never been set up so the session state has pristine value + if (!this.value.isPresent()) { + return true; + } + + // the pristine value isn't setup, so it's inconclusive. + // take the safest path + if (!this.pristineValue.isPresent()) { + return false; + } + + return this.value.get().equals(this.pristineValue.get()); + } + + public boolean canRestorePristine() { + if (!this.pristineValue.isPresent()) { + return false; + } + if (this.value.isPresent()) { + // it's necessary to restore pristine value only if current session value is not the same as pristine value. + return !(this.value.get().equals(this.pristineValue.get())); + } + + // it's inconclusive if the current value is the same as pristine value, so we need to take the safest path. + return true; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateHelper.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateHelper.java deleted file mode 100644 index d4b203593..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateHelper.java +++ /dev/null @@ -1,99 +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.states; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.EnumSet; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class SessionStateHelper { - - /** - * Transfers session state from source connection to destination connection. - * - * @param sessionState Session state of source connection - * @param src The source connection to transfer state from - * @param dest The destination connection to transfer state to - * @throws SQLException if a database access error occurs, this method is called on a closed connection, this - * method is called during a distributed transaction, or this method is called during a - * transaction - */ - public void transferSessionState( - final EnumSet sessionState, - final Connection src, - final Connection dest) throws SQLException { - - if (src == null || dest == null) { - return; - } - - if (sessionState.contains(SessionDirtyFlag.READONLY)) { - dest.setReadOnly(src.isReadOnly()); - } - if (sessionState.contains(SessionDirtyFlag.AUTO_COMMIT)) { - dest.setAutoCommit(src.getAutoCommit()); - } - if (sessionState.contains(SessionDirtyFlag.TRANSACTION_ISOLATION)) { - dest.setTransactionIsolation(src.getTransactionIsolation()); - } - if (sessionState.contains(SessionDirtyFlag.CATALOG)) { - dest.setCatalog(src.getCatalog()); - } - if (sessionState.contains(SessionDirtyFlag.SCHEMA)) { - dest.setSchema(src.getSchema()); - } - if (sessionState.contains(SessionDirtyFlag.TYPE_MAP)) { - dest.setTypeMap(src.getTypeMap()); - } - if (sessionState.contains(SessionDirtyFlag.HOLDABILITY)) { - dest.setHoldability(src.getHoldability()); - } - if (sessionState.contains(SessionDirtyFlag.NETWORK_TIMEOUT)) { - final ExecutorService executorService = Executors.newSingleThreadExecutor(); - dest.setNetworkTimeout(executorService, src.getNetworkTimeout()); - executorService.shutdown(); - } - } - - /** - * Restores partial session state from saved values to a connection. - * - * @param dest The destination connection to transfer state to - * @param readOnly ReadOnly flag to set to - * @param autoCommit AutoCommit flag to set to - * @throws SQLException if a database access error occurs, this method is called on a closed connection, this - * method is called during a distributed transaction, or this method is called during a - * transaction - */ - public void restoreSessionState(final Connection dest, final Boolean readOnly, final Boolean autoCommit) - throws SQLException { - - if (dest == null) { - return; - } - - if (readOnly != null) { - dest.setReadOnly(readOnly); - } - if (autoCommit != null) { - dest.setAutoCommit(autoCommit); - } - } - -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateService.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateService.java new file mode 100644 index 000000000..f4d2fc9fc --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateService.java @@ -0,0 +1,102 @@ +/* + * 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.states; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; + +public interface SessionStateService { + + // auto commit + Optional getAutoCommit() throws SQLException; + + void setAutoCommit(final boolean autoCommit) throws SQLException; + + void setupPristineAutoCommit() throws SQLException; + + // read-only + Optional getReadOnly() throws SQLException; + + void setReadOnly(boolean readOnly) throws SQLException; + + void setupPristineReadOnly() throws SQLException; + + // catalog + + Optional getCatalog() throws SQLException; + + void setCatalog(final String catalog) throws SQLException; + + void setupPristineCatalog() throws SQLException; + + // holdability + + Optional getHoldability() throws SQLException; + + void setHoldability(final int holdability) throws SQLException; + + void setupPristineHoldability() throws SQLException; + + // network timeout + + Optional getNetworkTimeout() throws SQLException; + + void setNetworkTimeout(final int milliseconds) throws SQLException; + + void setupPristineNetworkTimeout() throws SQLException; + + // schema + + Optional getSchema() throws SQLException; + + void setSchema(final String schema) throws SQLException; + + void setupPristineSchema() throws SQLException; + + // transaction isolation + + Optional getTransactionIsolation() throws SQLException; + + void setTransactionIsolation(final int level) throws SQLException; + + void setupPristineTransactionIsolation() throws SQLException; + + // type map + + Optional>> getTypeMap() throws SQLException; + + void setTypeMap(final Map> map) throws SQLException; + + void setupPristineTypeMap() throws SQLException; + + void reset(); + + // Begin session transfer process + void begin() throws SQLException; + + // Complete session transfer process. This method should be called despite whether + // session transfer is successful or not. + void complete(); + + // Apply current session state (of the current connection) to a new connection. + void applyCurrentSessionState(final Connection newConnection) throws SQLException; + + // Apply pristine values to the provided connection (practically resetting the connection to its original state). + void applyPristineSessionState(final Connection connection) throws SQLException; +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateServiceImpl.java new file mode 100644 index 000000000..9aea2c471 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateServiceImpl.java @@ -0,0 +1,440 @@ +/* + * 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.states; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.jdbc.Driver; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.PropertyDefinition; + +public class SessionStateServiceImpl implements SessionStateService { + + protected SessionState sessionState; + protected SessionState copySessionState; + + protected final PluginService pluginService; + protected final Properties props; + + + public SessionStateServiceImpl( + final @NonNull PluginService pluginService, + final @NonNull Properties props) { + + this.sessionState = new SessionState(); + this.copySessionState = null; + this.pluginService = pluginService; + this.props = props; + } + + protected boolean transferStateEnabledSetting() { + return PropertyDefinition.TRANSFER_SESSION_STATE_ON_SWITCH.getBoolean(this.props); + } + + protected boolean resetStateEnabledSetting() { + return PropertyDefinition.RESET_SESSION_STATE_ON_CLOSE.getBoolean(this.props); + } + + @Override + public Optional getAutoCommit() throws SQLException { + return this.sessionState.autoCommit.getValue(); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.autoCommit.setValue(autoCommit); + } + + @Override + public void setupPristineAutoCommit() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + + if (this.sessionState.autoCommit.getPristineValue().isPresent()) { + return; + } + this.sessionState.autoCommit.setPristineValue(this.pluginService.getCurrentConnection().getAutoCommit()); + } + + @Override + public Optional getReadOnly() throws SQLException { + return this.sessionState.readOnly.getValue(); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.readOnly.setValue(readOnly); + } + + @Override + public void setupPristineReadOnly() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.readOnly.getPristineValue().isPresent()) { + return; + } + this.sessionState.readOnly.setPristineValue(this.pluginService.getCurrentConnection().isReadOnly()); + } + + @Override + public Optional getCatalog() throws SQLException { + return this.sessionState.catalog.getValue(); + } + + @Override + public void setCatalog(String catalog) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.catalog.setValue(catalog); + } + + @Override + public void setupPristineCatalog() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.catalog.getPristineValue().isPresent()) { + return; + } + this.sessionState.catalog.setPristineValue(this.pluginService.getCurrentConnection().getCatalog()); + } + + @Override + public Optional getHoldability() throws SQLException { + return this.sessionState.holdability.getValue(); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.holdability.setValue(holdability); + } + + @Override + public void setupPristineHoldability() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.holdability.getPristineValue().isPresent()) { + return; + } + this.sessionState.holdability.setPristineValue(this.pluginService.getCurrentConnection().getHoldability()); + } + + @Override + public Optional getNetworkTimeout() throws SQLException { + return this.sessionState.networkTimeout.getValue(); + } + + @Override + public void setNetworkTimeout(int milliseconds) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.networkTimeout.setValue(milliseconds); + } + + @Override + public void setupPristineNetworkTimeout() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.networkTimeout.getPristineValue().isPresent()) { + return; + } + this.sessionState.networkTimeout.setPristineValue(this.pluginService.getCurrentConnection().getNetworkTimeout()); + } + + @Override + public Optional getSchema() throws SQLException { + return this.sessionState.schema.getValue(); + } + + @Override + public void setSchema(String schema) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.schema.setValue(schema); + } + + @Override + public void setupPristineSchema() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.schema.getPristineValue().isPresent()) { + return; + } + this.sessionState.schema.setPristineValue(this.pluginService.getCurrentConnection().getSchema()); + } + + @Override + public Optional getTransactionIsolation() throws SQLException { + return this.sessionState.transactionIsolation.getValue(); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.transactionIsolation.setValue(level); + } + + @Override + public void setupPristineTransactionIsolation() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.transactionIsolation.getPristineValue().isPresent()) { + return; + } + this.sessionState.transactionIsolation.setPristineValue( + this.pluginService.getCurrentConnection().getTransactionIsolation()); + } + + @Override + public Optional>> getTypeMap() throws SQLException { + return this.sessionState.typeMap.getValue(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + this.sessionState.typeMap.setValue(map); + } + + @Override + public void setupPristineTypeMap() throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + if (this.sessionState.typeMap.getPristineValue().isPresent()) { + return; + } + this.sessionState.typeMap.setPristineValue(this.pluginService.getCurrentConnection().getTypeMap()); + } + + @Override + public void reset() { + this.sessionState.autoCommit.reset(); + this.sessionState.readOnly.reset(); + this.sessionState.catalog.reset(); + this.sessionState.schema.reset(); + this.sessionState.holdability.reset(); + this.sessionState.networkTimeout.reset(); + this.sessionState.transactionIsolation.reset(); + this.sessionState.typeMap.reset(); + } + + @Override + public void begin() throws SQLException { + if (!this.transferStateEnabledSetting() && !this.resetStateEnabledSetting()) { + return; + } + + if (this.copySessionState != null) { + throw new SQLException("Previous session state transfer is not completed."); + } + + this.copySessionState = this.sessionState.copy(); + } + + @Override + public void complete() { + this.copySessionState = null; + } + + @Override + public void applyCurrentSessionState(Connection newConnection) throws SQLException { + if (!this.transferStateEnabledSetting()) { + return; + } + + TransferSessionStateOnSwitchCallable callableCopy = Driver.getTransferSessionStateOnSwitchFunc(); + if (callableCopy != null) { + final boolean isHandled = callableCopy.apply(sessionState, newConnection); + if (isHandled) { + // Custom function has handled session transfer + return; + } + } + + if (this.sessionState.autoCommit.getValue().isPresent()) { + this.sessionState.autoCommit.resetPristineValue(); + this.setupPristineAutoCommit(); + newConnection.setAutoCommit(this.sessionState.autoCommit.getValue().get()); + } + + if (this.sessionState.readOnly.getValue().isPresent()) { + this.sessionState.readOnly.resetPristineValue(); + this.setupPristineReadOnly(); + newConnection.setReadOnly(this.sessionState.readOnly.getValue().get()); + } + + if (this.sessionState.catalog.getValue().isPresent()) { + this.sessionState.catalog.resetPristineValue(); + this.setupPristineCatalog(); + newConnection.setCatalog(this.sessionState.catalog.getValue().get()); + } + + if (this.sessionState.schema.getValue().isPresent()) { + this.sessionState.schema.resetPristineValue(); + this.setupPristineSchema(); + newConnection.setSchema(this.sessionState.schema.getValue().get()); + } + + if (this.sessionState.holdability.getValue().isPresent()) { + this.sessionState.holdability.resetPristineValue(); + this.setupPristineHoldability(); + newConnection.setHoldability(this.sessionState.holdability.getValue().get()); + } + + if (this.sessionState.transactionIsolation.getValue().isPresent()) { + this.sessionState.transactionIsolation.resetPristineValue(); + this.setupPristineTransactionIsolation(); + //noinspection MagicConstant + newConnection.setTransactionIsolation(this.sessionState.transactionIsolation.getValue().get()); + } + + if (this.sessionState.networkTimeout.getValue().isPresent()) { + this.sessionState.networkTimeout.resetPristineValue(); + this.setupPristineNetworkTimeout(); + final ExecutorService executorService = Executors.newSingleThreadExecutor(); + newConnection.setNetworkTimeout(executorService, this.sessionState.networkTimeout.getValue().get()); + executorService.shutdown(); + } + + if (this.sessionState.typeMap.getValue().isPresent()) { + this.sessionState.typeMap.resetPristineValue(); + this.setupPristineTypeMap(); + newConnection.setTypeMap(this.sessionState.typeMap.getValue().get()); + } + } + + @Override + public void applyPristineSessionState(Connection connection) throws SQLException { + if (!this.resetStateEnabledSetting()) { + return; + } + + ResetSessionStateOnCloseCallable callableCopy = Driver.getResetSessionStateOnCloseFunc(); + if (callableCopy != null) { + final boolean isHandled = callableCopy.apply(sessionState, connection); + if (isHandled) { + // Custom function has handled session transfer + return; + } + } + + if (this.copySessionState.autoCommit.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setAutoCommit(this.copySessionState.autoCommit.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.readOnly.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setReadOnly(this.copySessionState.readOnly.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.catalog.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setCatalog(this.copySessionState.catalog.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.schema.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setSchema(this.copySessionState.schema.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.holdability.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setHoldability(this.copySessionState.holdability.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.transactionIsolation.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent,MagicConstant + connection.setTransactionIsolation( + this.copySessionState.transactionIsolation.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.networkTimeout.canRestorePristine()) { + try { + final ExecutorService executorService = Executors.newSingleThreadExecutor(); + //noinspection OptionalGetWithoutIsPresent + connection.setNetworkTimeout(executorService, + this.copySessionState.networkTimeout.getPristineValue().get()); + executorService.shutdown(); + } catch (final SQLException e) { + // Ignore any exception + } + } + + if (this.copySessionState.typeMap.canRestorePristine()) { + try { + //noinspection OptionalGetWithoutIsPresent + connection.setTypeMap(this.copySessionState.typeMap.getPristineValue().get()); + } catch (final SQLException e) { + // Ignore any exception + } + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateTransferCallable.java b/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateTransferCallable.java deleted file mode 100644 index 6872bb66c..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/states/SessionStateTransferCallable.java +++ /dev/null @@ -1,45 +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.states; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.EnumSet; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import software.amazon.jdbc.HostSpec; - -public interface SessionStateTransferCallable { - - /** - * Transfers session state from one connection to another. - * - * @param sessionState Session state flags for from-connection - * @param src The source connection to transfer state from - * @param srcHostSpec The source connection {@link HostSpec} - * @param dest The destination connection to transfer state to - * @param destHostSpec The destination connection {@link HostSpec} - * @return true, if session state transfer is successful and no default logic should be executed after. - * False, if default logic should be executed. - */ - boolean transferSessionState( - final @NonNull EnumSet sessionState, - final @NonNull Connection src, - final @Nullable HostSpec srcHostSpec, - final @NonNull Connection dest, - final @Nullable HostSpec destHostSpec) throws SQLException; -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/states/TransferSessionStateOnSwitchCallable.java b/wrapper/src/main/java/software/amazon/jdbc/states/TransferSessionStateOnSwitchCallable.java new file mode 100644 index 000000000..73990a2ad --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/states/TransferSessionStateOnSwitchCallable.java @@ -0,0 +1,26 @@ +/* + * 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.states; + +import java.sql.Connection; +import java.sql.SQLException; +import org.checkerframework.checker.nullness.qual.NonNull; + +public interface TransferSessionStateOnSwitchCallable { + boolean apply(final @NonNull SessionState sessionState, final @NonNull Connection newConnection) + throws SQLException; +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/GenericTargetDriverDialect.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/GenericTargetDriverDialect.java index dcecee90a..4b8d2402c 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/GenericTargetDriverDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/GenericTargetDriverDialect.java @@ -18,6 +18,7 @@ import static software.amazon.jdbc.util.ConnectionUrlBuilder.buildUrl; +import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; import java.util.Properties; @@ -26,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; public class GenericTargetDriverDialect implements TargetDriverDialect { @@ -86,4 +88,20 @@ public void prepareDataSource( } } + public boolean isDriverRegistered() throws SQLException { + throw new SQLException(Messages.get("TargetDriverDialect.unsupported")); + } + + public void registerDriver() throws SQLException { + throw new SQLException(Messages.get("TargetDriverDialect.unsupported")); + } + + @Override + public boolean ping(@NonNull Connection connection) { + try { + return connection.isValid(10); // 10s + } catch (SQLException e) { + return false; + } + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDataSourceHelper.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDriverHelper.java similarity index 66% rename from wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDataSourceHelper.java rename to wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDriverHelper.java index c01dbaef9..5c14cb3e3 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDataSourceHelper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbDriverHelper.java @@ -18,8 +18,12 @@ import static software.amazon.jdbc.util.ConnectionUrlBuilder.buildUrl; +import com.mysql.cj.jdbc.Driver; +import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Collections; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.sql.DataSource; import org.checkerframework.checker.nullness.qual.NonNull; @@ -27,11 +31,12 @@ import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.PropertyUtils; -public class MariadbDataSourceHelper { +public class MariadbDriverHelper { private static final Logger LOGGER = - Logger.getLogger(MariadbDataSourceHelper.class.getName()); + Logger.getLogger(MariadbDriverHelper.class.getName()); private static final String LOGIN_TIMEOUT = "loginTimeout"; private static final String DS_CLASS_NAME = MariaDbDataSource.class.getName(); @@ -59,12 +64,38 @@ public void prepareDataSource( props.remove(LOGIN_TIMEOUT); } + Integer loginTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.LOGIN_TIMEOUT); + if (loginTimeout != null) { + mariaDbDataSource.setLoginTimeout((int) TimeUnit.MILLISECONDS.toSeconds(loginTimeout)); + } + // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) // and include them to connect URL. - PropertyDefinition.removeAllExcept(props, PropertyDefinition.DATABASE.name); + PropertyDefinition.removeAllExcept(props, + PropertyDefinition.DATABASE.name, + PropertyDefinition.TCP_KEEP_ALIVE.name, + PropertyDefinition.CONNECT_TIMEOUT.name, + PropertyDefinition.SOCKET_TIMEOUT.name); String finalUrl = buildUrl(protocol, hostSpec, props); LOGGER.finest(() -> "Connecting to " + finalUrl); mariaDbDataSource.setUrl(finalUrl); } + + public boolean isDriverRegistered() throws SQLException { + return Collections.list(DriverManager.getDrivers()) + .stream() + .filter(x -> x instanceof org.mariadb.jdbc.Driver) + .map(x -> true) + .findAny() + .orElse(false); + } + + public void registerDriver() throws SQLException { + try { + DriverManager.registerDriver(new org.mariadb.jdbc.Driver()); + } catch (SQLException e) { + throw new SQLException(Messages.get("MariadbDriverHelper.canNotRegister"), e); + } + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbTargetDriverDialect.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbTargetDriverDialect.java index c380c99f3..bd4b9bfa7 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbTargetDriverDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MariadbTargetDriverDialect.java @@ -56,7 +56,12 @@ public ConnectInfo prepareConnectInfo(final @NonNull String protocol, // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) // and use them to make a connection props.remove(PERMIT_MYSQL_SCHEME); - PropertyDefinition.removeAllExceptCredentials(props); + PropertyDefinition.removeAllExcept(props, + PropertyDefinition.USER.name, + PropertyDefinition.PASSWORD.name, + PropertyDefinition.TCP_KEEP_ALIVE.name, + PropertyDefinition.CONNECT_TIMEOUT.name, + PropertyDefinition.SOCKET_TIMEOUT.name); // "permitMysqlScheme" should be in Url rather than in properties. String urlBuilder = protocol + hostSpec.getUrl() + databaseName @@ -74,7 +79,19 @@ public void prepareDataSource( // The logic is isolated to a separated class since it uses // direct reference to org.mariadb.jdbc.MariaDbDataSource - final MariadbDataSourceHelper helper = new MariadbDataSourceHelper(); + final MariadbDriverHelper helper = new MariadbDriverHelper(); helper.prepareDataSource(dataSource, protocol, hostSpec, props); } + + @Override + public boolean isDriverRegistered() throws SQLException { + final MariadbDriverHelper helper = new MariadbDriverHelper(); + return helper.isDriverRegistered(); + } + + @Override + public void registerDriver() throws SQLException { + final MariadbDriverHelper helper = new MariadbDriverHelper(); + helper.registerDriver(); + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDataSourceHelper.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDriverHelper.java similarity index 63% rename from wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDataSourceHelper.java rename to wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDriverHelper.java index be281830e..7812175f3 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDataSourceHelper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJDriverHelper.java @@ -16,9 +16,13 @@ package software.amazon.jdbc.targetdriverdialect; +import com.mysql.cj.jdbc.Driver; import com.mysql.cj.jdbc.MysqlDataSource; +import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Collections; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.sql.DataSource; import org.checkerframework.checker.nullness.qual.NonNull; @@ -27,10 +31,10 @@ import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; -public class MysqlConnectorJDataSourceHelper { +public class MysqlConnectorJDriverHelper { private static final Logger LOGGER = - Logger.getLogger(MysqlConnectorJDataSourceHelper.class.getName()); + Logger.getLogger(MysqlConnectorJDriverHelper.class.getName()); public void prepareDataSource( final @NonNull DataSource dataSource, @@ -54,10 +58,37 @@ public void prepareDataSource( baseDataSource.setPortNumber(hostSpec.getPort()); } + Integer loginTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.LOGIN_TIMEOUT); + if (loginTimeout != null) { + baseDataSource.setLoginTimeout((int) TimeUnit.MILLISECONDS.toSeconds(loginTimeout)); + } + // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) // and try to apply them to data source - PropertyDefinition.removeAll(props); + PropertyDefinition.removeAllExcept(props, + PropertyDefinition.USER.name, + PropertyDefinition.PASSWORD.name, + PropertyDefinition.TCP_KEEP_ALIVE.name, + PropertyDefinition.SOCKET_TIMEOUT.name, + PropertyDefinition.CONNECT_TIMEOUT.name); PropertyUtils.applyProperties(dataSource, props); } + + public boolean isDriverRegistered() throws SQLException { + return Collections.list(DriverManager.getDrivers()) + .stream() + .filter(x -> x instanceof com.mysql.cj.jdbc.Driver) + .map(x -> true) + .findAny() + .orElse(false); + } + + public void registerDriver() throws SQLException { + try { + DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver()); + } catch (SQLException e) { + throw new SQLException(Messages.get("MysqlConnectorJDriverHelper.canNotRegister"), e); + } + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJTargetDriverDialect.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJTargetDriverDialect.java index 7dd971a92..b5ca755c1 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJTargetDriverDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/MysqlConnectorJTargetDriverDialect.java @@ -16,12 +16,16 @@ package software.amazon.jdbc.targetdriverdialect; +import java.sql.Connection; import java.sql.Driver; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; public class MysqlConnectorJTargetDriverDialect extends GenericTargetDriverDialect { @@ -40,6 +44,29 @@ public boolean isDialect(String dataSourceClass) { || CP_DS_CLASS_NAME.equals(dataSourceClass); } + @Override + public ConnectInfo prepareConnectInfo(final @NonNull String protocol, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props) throws SQLException { + + final String databaseName = + PropertyDefinition.DATABASE.getString(props) != null + ? PropertyDefinition.DATABASE.getString(props) + : ""; + String urlBuilder = protocol + hostSpec.getUrl() + databaseName; + + // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) + // and use them to make a connection + PropertyDefinition.removeAllExcept(props, + PropertyDefinition.USER.name, + PropertyDefinition.PASSWORD.name, + PropertyDefinition.TCP_KEEP_ALIVE.name, + PropertyDefinition.SOCKET_TIMEOUT.name, + PropertyDefinition.CONNECT_TIMEOUT.name); + + return new ConnectInfo(urlBuilder, props); + } + @Override public void prepareDataSource( final @NonNull DataSource dataSource, @@ -49,7 +76,31 @@ public void prepareDataSource( // The logic is isolated to a separated class since it uses // direct reference to com.mysql.cj.jdbc.MysqlDataSource - final MysqlConnectorJDataSourceHelper helper = new MysqlConnectorJDataSourceHelper(); + final MysqlConnectorJDriverHelper helper = new MysqlConnectorJDriverHelper(); helper.prepareDataSource(dataSource, hostSpec, props); } + + @Override + public boolean isDriverRegistered() throws SQLException { + final MysqlConnectorJDriverHelper helper = new MysqlConnectorJDriverHelper(); + return helper.isDriverRegistered(); + } + + @Override + public void registerDriver() throws SQLException { + final MysqlConnectorJDriverHelper helper = new MysqlConnectorJDriverHelper(); + helper.registerDriver(); + } + + @Override + public boolean ping(@NonNull Connection connection) { + try { + try (final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery("/* ping */ SELECT 1")) { + return true; + } + } catch (SQLException e) { + return false; + } + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDataSourceHelper.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDriverHelper.java similarity index 64% rename from wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDataSourceHelper.java rename to wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDriverHelper.java index 94a262d38..a828245ce 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDataSourceHelper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgDriverHelper.java @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.sql.DataSource; import org.checkerframework.checker.nullness.qual.NonNull; @@ -27,10 +28,10 @@ import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; -public class PgDataSourceHelper { +public class PgDriverHelper { private static final Logger LOGGER = - Logger.getLogger(PgDataSourceHelper.class.getName()); + Logger.getLogger(PgDriverHelper.class.getName()); private static final String BASE_DS_CLASS_NAME = org.postgresql.ds.common.BaseDataSource.class.getName(); @@ -57,10 +58,38 @@ public void prepareDataSource( baseDataSource.setPortNumbers(new int[] { hostSpec.getPort() }); } + final Boolean tcpKeepAlive = PropertyUtils.getBooleanPropertyValue(props, PropertyDefinition.TCP_KEEP_ALIVE); + if (tcpKeepAlive != null) { + baseDataSource.setTcpKeepAlive(tcpKeepAlive); + } + + final Integer loginTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.LOGIN_TIMEOUT); + if (loginTimeout != null) { + baseDataSource.setLoginTimeout((int) TimeUnit.MILLISECONDS.toSeconds(loginTimeout)); + } + + final Integer connectTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.CONNECT_TIMEOUT); + if (connectTimeout != null) { + baseDataSource.setConnectTimeout((int) TimeUnit.MILLISECONDS.toSeconds(connectTimeout)); + } + + final Integer socketTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.SOCKET_TIMEOUT); + if (socketTimeout != null) { + baseDataSource.setSocketTimeout((int) TimeUnit.MILLISECONDS.toSeconds(socketTimeout)); + } + // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) // and try to apply them to data source PropertyDefinition.removeAll(props); PropertyUtils.applyProperties(dataSource, props); } + + public boolean isDriverRegistered() throws SQLException { + return org.postgresql.Driver.isRegistered(); + } + + public void registerDriver() throws SQLException { + org.postgresql.Driver.register(); + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgTargetDriverDialect.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgTargetDriverDialect.java index 2862aa80a..88869015e 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgTargetDriverDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/PgTargetDriverDialect.java @@ -22,9 +22,13 @@ import java.util.HashSet; import java.util.Properties; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.sql.DataSource; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.PropertyUtils; public class PgTargetDriverDialect extends GenericTargetDriverDialect { @@ -48,6 +52,47 @@ public boolean isDialect(String dataSourceClass) { return dataSourceClassMap.contains(dataSourceClass); } + @Override + public ConnectInfo prepareConnectInfo(final @NonNull String protocol, + final @NonNull HostSpec hostSpec, + final @NonNull Properties props) throws SQLException { + + final String databaseName = + PropertyDefinition.DATABASE.getString(props) != null + ? PropertyDefinition.DATABASE.getString(props) + : ""; + + final Boolean tcpKeepAlive = PropertyUtils.getBooleanPropertyValue(props, PropertyDefinition.TCP_KEEP_ALIVE); + final Integer loginTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.LOGIN_TIMEOUT); + final Integer connectTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.CONNECT_TIMEOUT); + final Integer socketTimeout = PropertyUtils.getIntegerPropertyValue(props, PropertyDefinition.SOCKET_TIMEOUT); + + // keep unknown properties (the ones that don't belong to AWS Wrapper Driver) + // and use them to make a connection + PropertyDefinition.removeAllExceptCredentials(props); + + if (tcpKeepAlive != null) { + props.setProperty("tcpKeepAlive", String.valueOf(tcpKeepAlive)); + } + + if (loginTimeout != null) { + props.setProperty("loginTimeout", + String.valueOf(TimeUnit.MILLISECONDS.toSeconds(loginTimeout))); + } + if (connectTimeout != null) { + props.setProperty("connectTimeout", + String.valueOf(TimeUnit.MILLISECONDS.toSeconds(connectTimeout))); + } + if (socketTimeout != null) { + props.setProperty("socketTimeout", + String.valueOf(TimeUnit.MILLISECONDS.toSeconds(socketTimeout))); + } + + String urlBuilder = protocol + hostSpec.getUrl() + databaseName; + + return new ConnectInfo(urlBuilder, props); + } + @Override public void prepareDataSource( final @NonNull DataSource dataSource, @@ -57,7 +102,19 @@ public void prepareDataSource( // The logic is isolated to a separated class since it uses // direct reference to org.postgresql.ds.common.BaseDataSource - final PgDataSourceHelper helper = new PgDataSourceHelper(); + final PgDriverHelper helper = new PgDriverHelper(); helper.prepareDataSource(dataSource, hostSpec, props); } + + @Override + public boolean isDriverRegistered() throws SQLException { + final PgDriverHelper helper = new PgDriverHelper(); + return helper.isDriverRegistered(); + } + + @Override + public void registerDriver() throws SQLException { + final PgDriverHelper helper = new PgDriverHelper(); + helper.registerDriver(); + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialect.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialect.java index 06381992f..9a2483f2b 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialect.java @@ -16,6 +16,7 @@ package software.amazon.jdbc.targetdriverdialect; +import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource; @@ -37,4 +38,19 @@ void prepareDataSource( final @NonNull String protocol, final @NonNull HostSpec hostSpec, final @NonNull Properties props) throws SQLException; + + boolean isDriverRegistered() throws SQLException; + + void registerDriver() throws SQLException; + + /** + * Attempts to communicate to a database node in order to measure network latency. + * Some database protocols may not support the simplest "ping" packet. In this case, + * it's recommended to execute a simple connection validation, or the simplest SQL + * query like "SELECT 1". + * + * @param connection The database connection to a node to ping. + * @return True, if operation is succeeded. False, otherwise. + */ + boolean ping(final @NonNull Connection connection); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectManager.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectManager.java index b31348b6b..63501bff7 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectManager.java @@ -26,6 +26,7 @@ import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.AwsWrapperProperty; +import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.StringUtils; @@ -39,6 +40,10 @@ public class TargetDriverDialectManager implements TargetDriverDialectProvider { "wrapperTargetDriverDialect", "", "A unique identifier for the target driver dialect."); + public static final AwsWrapperProperty TARGET_DRIVER_AUTO_REGISTER = new AwsWrapperProperty( + "targetDriverAutoRegister", "true", + "Allows to auto-register a target driver."); + /** * Every Dialect implementation SHOULD BE stateless!!! * Dialect objects are shared between different connections. @@ -54,6 +59,19 @@ public class TargetDriverDialectManager implements TargetDriverDialectProvider { } }; + protected static final Map defaultDialectsByProtocol = + new HashMap() { + { + put("jdbc:postgresql://", new PgTargetDriverDialect()); + put("jdbc:mysql://", new MysqlConnectorJTargetDriverDialect()); + put("jdbc:mariadb://", new MariadbTargetDriverDialect()); + } + }; + + static { + PropertyDefinition.registerPluginProperties(TargetDriverDialectManager.class); + } + public static void setCustomDialect(final @NonNull TargetDriverDialect targetDriverDialect) { customDialect = targetDriverDialect; } @@ -121,4 +139,56 @@ private void logDialect(final String dialectCode, final TargetDriverDialect targ "TargetDriverDialectManager.useDialect", new Object[] {dialectCode, targetDriverDialect})); } + + /** + * Tries to identify a driver corresponded to provided protocol and register it. + * Driver registration may be disabled by provided configuration properties. + * + * @param protocol The protocol to identify a corresponding driver for registration. + * @param props The properties + * @return True, if a corresponding driver was found and registered. + * False, otherwise. + * @throws SQLException when user provided invalid target driver dialect code, + * or when provided protocol is not recognized. + */ + public boolean registerDriver( + final @NonNull String protocol, + final @NonNull Properties props) throws SQLException { + + if (!TARGET_DRIVER_AUTO_REGISTER.getBoolean(props)) { + // Driver auto-registration isn't allowed. + return false; + } + + TargetDriverDialect targetDriverDialect = null; + + // Try to get a target driver dialect provided by the user. + String dialectCode = TARGET_DRIVER_DIALECT.getString(props); + if (!StringUtils.isNullOrEmpty(dialectCode)) { + targetDriverDialect = knownDialectsByCode.get(dialectCode); + if (targetDriverDialect == null) { + throw new SQLException(Messages.get( + "TargetDriverDialectManager.unknownDialectCode", + new Object[] {dialectCode})); + } + } + + // Target driver dialect isn't found (or it's not provided by the user). + // Try to find a dialect by provided protocol. + if (targetDriverDialect == null) { + targetDriverDialect = defaultDialectsByProtocol.get(protocol.toLowerCase()); + if (targetDriverDialect == null) { + throw new SQLException(Messages.get( + "TargetDriverDialectManager.unknownProtocol", + new Object[] {protocol.toLowerCase()})); + } + } + + // Check if a driver associated with found dialect is registered. Register it if needed. + if (!targetDriverDialect.isDriverRegistered()) { + targetDriverDialect.registerDriver(); + } + + return true; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectProvider.java b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectProvider.java index 241e41a7a..9fd2cc3f4 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/targetdriverdialect/TargetDriverDialectProvider.java @@ -30,4 +30,8 @@ TargetDriverDialect getDialect( TargetDriverDialect getDialect( final @NonNull String dataSourceClass, final @NonNull Properties props) throws SQLException; + + boolean registerDriver( + final @NonNull String protocol, + final @NonNull Properties props) throws SQLException; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/CacheMap.java b/wrapper/src/main/java/software/amazon/jdbc/util/CacheMap.java index eefe85567..d838f4c95 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/CacheMap.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/CacheMap.java @@ -24,9 +24,9 @@ public class CacheMap { - private final Map> cache = new ConcurrentHashMap<>(); - private final long cleanupIntervalNanos = TimeUnit.MINUTES.toNanos(10); - private final AtomicLong cleanupTimeNanos = new AtomicLong(System.nanoTime() + cleanupIntervalNanos); + protected final Map> cache = new ConcurrentHashMap<>(); + protected final long cleanupIntervalNanos = TimeUnit.MINUTES.toNanos(10); + protected final AtomicLong cleanupTimeNanos = new AtomicLong(System.nanoTime() + cleanupIntervalNanos); public CacheMap() { } @@ -75,18 +75,25 @@ public int size() { return this.cache.size(); } - private void cleanUp() { + protected void cleanUp() { if (this.cleanupTimeNanos.get() < System.nanoTime()) { this.cleanupTimeNanos.set(System.nanoTime() + cleanupIntervalNanos); cache.forEach((key, value) -> { if (value == null || value.isExpired()) { cache.remove(key); + if (value != null && value.item instanceof AutoCloseable) { + try { + ((AutoCloseable) value.item).close(); + } catch (Exception e) { + // ignore + } + } } }); } } - private static class CacheItem { + static class CacheItem { final V item; final long expirationTime; diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/ConnectionUrlParser.java b/wrapper/src/main/java/software/amazon/jdbc/util/ConnectionUrlParser.java index a5b2d8e3b..a59bb2c66 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/ConnectionUrlParser.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/ConnectionUrlParser.java @@ -27,7 +27,6 @@ import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; -import software.amazon.jdbc.hostavailability.HostAvailabilityStrategyFactory; public class ConnectionUrlParser { @@ -90,6 +89,8 @@ public static HostSpec parseHostPortPair(final String url, final HostRole role, private static HostSpec getHostSpec(final String[] hostPortPair, final HostRole hostRole, final HostSpecBuilder hostSpecBuilder) { + String hostId = rdsUtils.getRdsInstanceId(hostPortPair[0]); + if (hostPortPair.length > 1) { final String[] port = hostPortPair[1].split("/"); int portValue = parsePortAsInt(hostPortPair[1]); @@ -99,12 +100,14 @@ private static HostSpec getHostSpec(final String[] hostPortPair, final HostRole return hostSpecBuilder .host(hostPortPair[0]) .port(portValue) + .hostId(hostId) .role(hostRole) .build(); } return hostSpecBuilder .host(hostPortPair[0]) .port(HostSpec.NO_PORT) + .hostId(hostId) .role(hostRole) .build(); } @@ -197,4 +200,15 @@ public static void parsePropertiesFromUrl(final String url, final Properties pro // Attempt to use the original value for connection. return url; } + + public String getProtocol(final String url) { + final int index = url.indexOf("//"); + if (index < 0) { + throw new IllegalArgumentException( + Messages.get( + "ConnectionUrlParser.protocolNotFound", + new Object[] {url})); + } + return url.substring(0, index + 2); + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/IamAuthUtils.java b/wrapper/src/main/java/software/amazon/jdbc/util/IamAuthUtils.java new file mode 100644 index 000000000..af6b6af06 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/util/IamAuthUtils.java @@ -0,0 +1,38 @@ +/* + * 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 software.amazon.jdbc.HostSpec; + +public class IamAuthUtils { + public static String getIamHost(final String iamHost, final HostSpec hostSpec) { + if (!StringUtils.isNullOrEmpty(iamHost)) { + return iamHost; + } + return hostSpec.getHost(); + } + + public static int getIamPort(final int iamDefaultPort, final HostSpec hostSpec, final int dialectDefaultPort) { + if (iamDefaultPort > 0) { + return iamDefaultPort; + } else if (hostSpec.isPortSpecified()) { + return hostSpec.getPort(); + } else { + return dialectDefaultPort; + } + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/PropertyUtils.java b/wrapper/src/main/java/software/amazon/jdbc/util/PropertyUtils.java index defd967b7..ce2f66fad 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/PropertyUtils.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/PropertyUtils.java @@ -28,6 +28,8 @@ import java.util.Set; import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; +import software.amazon.awssdk.services.rds.endpoints.internal.Value.Bool; +import software.amazon.jdbc.AwsWrapperProperty; import software.amazon.jdbc.PropertyDefinition; public class PropertyUtils { @@ -128,10 +130,20 @@ public static void setPropertyOnTarget( return copy; } - for (final Map.Entry entry : props.entrySet()) { - copy.setProperty(entry.getKey().toString(), entry.getValue().toString()); + return addProperties(copy, props); + } + + public static @NonNull Properties addProperties( + final Properties dest, final Properties propsToAdd) { + + if (propsToAdd == null) { + return dest; } - return copy; + + for (final Map.Entry entry : propsToAdd.entrySet()) { + dest.setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + return dest; } private static boolean isSecretProperty(final Object propertyKey) { @@ -162,4 +174,26 @@ public static String logProperties(final Properties props, final String caption) } return sb.toString(); } + + public static Integer getIntegerPropertyValue( + final @NonNull Properties props, + final @NonNull AwsWrapperProperty wrapperProperty) { + + Integer result = null; + if (!StringUtils.isNullOrEmpty(wrapperProperty.getString(props))) { + result = wrapperProperty.getInteger(props); + } + return result; + } + + public static Boolean getBooleanPropertyValue( + final @NonNull Properties props, + final @NonNull AwsWrapperProperty wrapperProperty) { + + Boolean result = null; + if (!StringUtils.isNullOrEmpty(wrapperProperty.getString(props))) { + result = wrapperProperty.getBoolean(props); + } + return result; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/RdsUtils.java b/wrapper/src/main/java/software/amazon/jdbc/util/RdsUtils.java index 63f91a673..62b33c1d8 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/RdsUtils.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/RdsUtils.java @@ -18,6 +18,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; public class RdsUtils { @@ -141,6 +142,8 @@ public class RdsUtils { Pattern.compile( "^(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)" + "::(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)$"); + + private static final String INSTANCE_GROUP = "instance"; private static final String DNS_GROUP = "dns"; private static final String DOMAIN_GROUP = "domain"; private static final String REGION_GROUP = "region"; @@ -178,6 +181,21 @@ public boolean isElbUrl(final String host) { && (ELB_PATTERN.matcher(host).find()); } + public @Nullable String getRdsInstanceId(final String host) { + if (StringUtils.isNullOrEmpty(host)) { + return null; + } + final Matcher matcher = AURORA_INSTANCE_PATTERN.matcher(host); + if (matcher.find()) { + return matcher.group(INSTANCE_GROUP); + } + final Matcher matcherChina = AURORA_CHINA_INSTANCE_PATTERN.matcher(host); + if (matcherChina.find()) { + return matcherChina.group(INSTANCE_GROUP); + } + return null; + } + public String getRdsInstanceHostPattern(final String host) { if (StringUtils.isNullOrEmpty(host)) { return "?"; diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCache.java b/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCache.java index 5e5484195..df18c8461 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCache.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCache.java @@ -24,11 +24,12 @@ import java.util.function.Function; public class SlidingExpirationCache { - private final Map cache = new ConcurrentHashMap<>(); - private long cleanupIntervalNanos = TimeUnit.MINUTES.toNanos(10); - private final AtomicLong cleanupTimeNanos = new AtomicLong(System.nanoTime() + cleanupIntervalNanos); - private final ShouldDisposeFunc shouldDisposeFunc; - private final ItemDisposalFunc itemDisposalFunc; + + protected final Map cache = new ConcurrentHashMap<>(); + protected long cleanupIntervalNanos = TimeUnit.MINUTES.toNanos(10); + protected final AtomicLong cleanupTimeNanos = new AtomicLong(System.nanoTime() + cleanupIntervalNanos); + protected final ShouldDisposeFunc shouldDisposeFunc; + protected final ItemDisposalFunc itemDisposalFunc; /** * A cache that periodically cleans up expired entries. Fetching an expired entry marks that entry @@ -57,6 +58,15 @@ public SlidingExpirationCache( this.itemDisposalFunc = itemDisposalFunc; } + public SlidingExpirationCache( + final ShouldDisposeFunc shouldDisposeFunc, + final ItemDisposalFunc itemDisposalFunc, + final long cleanupIntervalNanos) { + this.shouldDisposeFunc = shouldDisposeFunc; + this.itemDisposalFunc = itemDisposalFunc; + this.cleanupIntervalNanos = cleanupIntervalNanos; + } + /** * In addition to performing the logic defined by {@link Map#computeIfAbsent}, cleans up expired * entries if we have hit cleanup time. If an expired entry is requested and we have not hit @@ -83,6 +93,12 @@ public V computeIfAbsent( return cacheItem.withExtendExpiration(itemExpirationNano).item; } + public V get(final K key, final long itemExpirationNano) { + cleanUp(); + final CacheItem cacheItem = cache.get(key); + return cacheItem == null ? null : cacheItem.withExtendExpiration(itemExpirationNano).item; + } + /** * Cleanup expired entries if we have hit the cleanup time, then remove and dispose the value * associated with the given key. @@ -94,14 +110,14 @@ public void remove(final K key) { cleanUp(); } - private void removeAndDispose(K key) { + protected void removeAndDispose(K key) { final CacheItem cacheItem = cache.remove(key); if (cacheItem != null && itemDisposalFunc != null) { itemDisposalFunc.dispose(cacheItem.item); } } - private void removeIfExpired(K key) { + protected void removeIfExpired(K key) { final CacheItem cacheItem = cache.get(key); if (cacheItem == null || cacheItem.shouldCleanup()) { removeAndDispose(key); @@ -140,7 +156,7 @@ public int size() { return this.cache.size(); } - private void cleanUp() { + protected void cleanUp() { if (this.cleanupTimeNanos.get() > System.nanoTime()) { return; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCacheWithCleanupThread.java b/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCacheWithCleanupThread.java new file mode 100644 index 000000000..e63ee0f18 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/util/SlidingExpirationCacheWithCleanupThread.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.util; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class SlidingExpirationCacheWithCleanupThread extends SlidingExpirationCache { + + private static final Logger LOGGER = + Logger.getLogger(SlidingExpirationCacheWithCleanupThread.class.getName()); + + protected static final ExecutorService cleanupThreadPool = Executors.newFixedThreadPool(1, runnableTarget -> { + final Thread monitoringThread = new Thread(runnableTarget); + monitoringThread.setDaemon(true); + return monitoringThread; + }); + + public SlidingExpirationCacheWithCleanupThread() { + super(); + this.initCleanupThread(); + } + + public SlidingExpirationCacheWithCleanupThread( + final ShouldDisposeFunc shouldDisposeFunc, + final ItemDisposalFunc itemDisposalFunc) { + super(shouldDisposeFunc, itemDisposalFunc); + this.initCleanupThread(); + } + + public SlidingExpirationCacheWithCleanupThread( + final ShouldDisposeFunc shouldDisposeFunc, + final ItemDisposalFunc itemDisposalFunc, + final long cleanupIntervalNanos) { + super(shouldDisposeFunc, itemDisposalFunc, cleanupIntervalNanos); + this.initCleanupThread(); + } + + protected void initCleanupThread() { + cleanupThreadPool.submit(() -> { + while (true) { + TimeUnit.NANOSECONDS.sleep(this.cleanupIntervalNanos); + + LOGGER.finest("Cleaning up..."); + this.cleanupTimeNanos.set(System.nanoTime() + cleanupIntervalNanos); + cache.forEach((key, value) -> { + try { + removeIfExpired(key); + } catch (Exception ex) { + // ignore + } + }); + } + }); + cleanupThreadPool.shutdown(); + } + + @Override + protected void cleanUp() { + // Intentionally do nothing. Cleanup thread does the job. + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/SqlMethodAnalyzer.java b/wrapper/src/main/java/software/amazon/jdbc/util/SqlMethodAnalyzer.java index 8dd8d2048..61f9adc97 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/SqlMethodAnalyzer.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/SqlMethodAnalyzer.java @@ -47,7 +47,11 @@ public boolean doesOpenTransaction(final Connection conn, final String methodNam } private String getFirstSqlStatement(final String sql) { - String statement = parseMultiStatementQueries(sql).get(0); + List statementList = parseMultiStatementQueries(sql); + if (statementList.isEmpty()) { + return sql; + } + String statement = statementList.get(0); statement = statement.toUpperCase(); statement = statement.replaceAll("\\s*/\\*(.*?)\\*/\\s*", " ").trim(); return statement; diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/telemetry/OpenTelemetryFactory.java b/wrapper/src/main/java/software/amazon/jdbc/util/telemetry/OpenTelemetryFactory.java index 9181eb755..d39307faa 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/telemetry/OpenTelemetryFactory.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/telemetry/OpenTelemetryFactory.java @@ -25,6 +25,14 @@ public class OpenTelemetryFactory implements TelemetryFactory { private static final String INSTRUMENTATION_NAME = "aws-advanced-jdbc-wrapper"; + /** + * Max allowed name length for counters and gauges. + * + * @see + * More details + */ + private static final int NAME_MAX_LENGTH = 63; + private static OpenTelemetry openTelemetry; private static Tracer tracer; private static Meter meter; @@ -57,13 +65,22 @@ public void postCopy(TelemetryContext telemetryContext, TelemetryTraceLevel trac } public TelemetryCounter createCounter(String name) { + if (name == null) { + throw new IllegalArgumentException("name"); + } meter = getOpenTelemetry().getMeter(INSTRUMENTATION_NAME); - return new OpenTelemetryCounter(meter, name); + return new OpenTelemetryCounter(meter, trimName(name)); } public TelemetryGauge createGauge(String name, GaugeCallable callback) { + if (name == null) { + throw new IllegalArgumentException("name"); + } meter = getOpenTelemetry().getMeter(INSTRUMENTATION_NAME); - return new OpenTelemetryGauge(meter, name, callback); + return new OpenTelemetryGauge(meter, trimName(name), callback); } + private String trimName(final String name) { + return (name.length() > NAME_MAX_LENGTH) ? name.substring(0, NAME_MAX_LENGTH) : name; + } } 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 a48cdbc55..d2405d288 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java @@ -48,7 +48,9 @@ import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.cleanup.CanReleaseResources; import software.amazon.jdbc.dialect.HostListProviderSupplier; -import software.amazon.jdbc.states.SessionDirtyFlag; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; +import software.amazon.jdbc.util.ConnectionUrlParser; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlState; import software.amazon.jdbc.util.StringUtils; @@ -67,13 +69,19 @@ public class ConnectionWrapper implements Connection, CanReleaseResources { protected PluginManagerService pluginManagerService; protected String targetDriverProtocol; // TODO: consider moving to PluginService protected String originalUrl; // TODO: consider moving to PluginService + protected @Nullable ConfigurationProfile configurationProfile; protected @Nullable Throwable openConnectionStacktrace; + protected final ConnectionUrlParser connectionUrlParser = new ConnectionUrlParser(); + public ConnectionWrapper( @NonNull final Properties props, @NonNull final String url, - @NonNull final ConnectionProvider connectionProvider, + @NonNull final ConnectionProvider defaultConnectionProvider, + @Nullable final ConnectionProvider effectiveConnectionProvider, + @NonNull final TargetDriverDialect targetDriverDialect, + @Nullable final ConfigurationProfile configurationProfile, @NonNull final TelemetryFactory telemetryFactory) throws SQLException { @@ -82,11 +90,17 @@ public ConnectionWrapper( } this.originalUrl = url; - this.targetDriverProtocol = getProtocol(url); + this.targetDriverProtocol = connectionUrlParser.getProtocol(url); + this.configurationProfile = configurationProfile; final ConnectionPluginManager pluginManager = - new ConnectionPluginManager(connectionProvider, this, telemetryFactory); - final PluginServiceImpl pluginService = new PluginServiceImpl(pluginManager, props, url, this.targetDriverProtocol); + new ConnectionPluginManager( + defaultConnectionProvider, + effectiveConnectionProvider, + this, + telemetryFactory); + final PluginServiceImpl pluginService = new PluginServiceImpl( + pluginManager, props, url, this.targetDriverProtocol, targetDriverDialect, this.configurationProfile); init(props, pluginManager, telemetryFactory, pluginService, pluginService, pluginService); @@ -127,7 +141,9 @@ protected void init( this.hostListProviderService = hostListProviderService; this.pluginManagerService = pluginManagerService; - this.pluginManager.init(this.pluginService, props, pluginManagerService); + this.pluginManager.init( + this.pluginService, props, pluginManagerService, this.configurationProfile); + final HostListProviderSupplier supplier = this.pluginService.getDialect().getHostListProvider(); if (supplier != null) { final HostListProvider provider = supplier.getProvider(props, this.originalUrl, hostListProviderService); @@ -152,17 +168,6 @@ protected void init( } } - protected String getProtocol(final String url) { - final int index = url.indexOf("//"); - if (index < 0) { - throw new IllegalArgumentException( - Messages.get( - "ConnectionWrapper.protocolNotFound", - new Object[] {url})); - } - return url.substring(0, index + 2); - } - public void releaseResources() { this.pluginManager.releaseResources(); if (this.pluginService instanceof CanReleaseResources) { @@ -180,7 +185,7 @@ public void abort(final Executor executor) throws SQLException { () -> { this.pluginService.getCurrentConnection().abort(executor); this.pluginManagerService.setInTransaction(false); - this.pluginService.resetCurrentConnectionStates(); + this.pluginService.getSessionStateService().reset(); }, executor); } @@ -203,10 +208,17 @@ public void close() throws SQLException { this.pluginService.getCurrentConnection(), "Connection.close", () -> { - this.pluginService.getCurrentConnection().close(); + this.pluginService.getSessionStateService().begin(); + try { + this.pluginService.getSessionStateService().applyPristineSessionState( + this.pluginService.getCurrentConnection()); + this.pluginService.getCurrentConnection().close(); + } finally { + this.pluginService.getSessionStateService().complete(); + this.pluginService.getSessionStateService().reset(); + } this.openConnectionStacktrace = null; this.pluginManagerService.setInTransaction(false); - this.pluginService.resetCurrentConnectionStates(); }); this.releaseResources(); } @@ -220,12 +232,11 @@ public void commit() throws SQLException { "Connection.commit", () -> { this.pluginService.getCurrentConnection().commit(); - final boolean isInTransaction = this.pluginService.isInTransaction(); this.pluginManagerService.setInTransaction(false); - if (isInTransaction - && this.pluginService.getCurrentConnectionState().contains(SessionDirtyFlag.AUTO_COMMIT)) { - this.pluginService.resetCurrentConnectionState(SessionDirtyFlag.AUTO_COMMIT); - } + + // After commit, autoCommit setting restores to the latest value set by user, + // and it is already tracked by session state service. + // No additional handling of autoCommit is required. }); } @@ -352,9 +363,9 @@ public void setReadOnly(final boolean readOnly) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setReadOnly", () -> { + this.pluginService.getSessionStateService().setupPristineReadOnly(); this.pluginService.getCurrentConnection().setReadOnly(readOnly); - this.pluginManagerService.setReadOnly(readOnly); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.READONLY); + this.pluginService.getSessionStateService().setReadOnly(readOnly); }, readOnly); } @@ -684,12 +695,11 @@ public void rollback() throws SQLException { "Connection.rollback", () -> { this.pluginService.getCurrentConnection().rollback(); - final boolean isInTransaction = this.pluginService.isInTransaction(); this.pluginManagerService.setInTransaction(false); - if (isInTransaction - && this.pluginService.getCurrentConnectionState().contains(SessionDirtyFlag.AUTO_COMMIT)) { - this.pluginService.resetCurrentConnectionState(SessionDirtyFlag.AUTO_COMMIT); - } + + // After rollback, autoCommit setting restores to the latest value set by user, + // and it is already tracked by session state service. + // No additional handling of autoCommit is required. }); } @@ -719,12 +729,9 @@ public void setAutoCommit(final boolean autoCommit) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setAutoCommit", () -> { - final boolean currentAutoCommit = this.pluginService.getAutoCommit(); + this.pluginService.getSessionStateService().setupPristineAutoCommit(); this.pluginService.getCurrentConnection().setAutoCommit(autoCommit); - this.pluginService.setAutoCommit(autoCommit); - if (currentAutoCommit != autoCommit) { - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.AUTO_COMMIT); - } + this.pluginService.getSessionStateService().setAutoCommit(autoCommit); }, autoCommit); } @@ -748,8 +755,9 @@ public void setCatalog(final String catalog) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setCatalog", () -> { + this.pluginService.getSessionStateService().setupPristineCatalog(); this.pluginService.getCurrentConnection().setCatalog(catalog); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.CATALOG); + this.pluginService.getSessionStateService().setCatalog(catalog); }, catalog); } @@ -785,8 +793,9 @@ public void setHoldability(final int holdability) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setHoldability", () -> { + this.pluginService.getSessionStateService().setupPristineHoldability(); this.pluginService.getCurrentConnection().setHoldability(holdability); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.HOLDABILITY); + this.pluginService.getSessionStateService().setHoldability(holdability); }, holdability); } @@ -799,8 +808,9 @@ public void setNetworkTimeout(final Executor executor, final int milliseconds) t this.pluginService.getCurrentConnection(), "Connection.setNetworkTimeout", () -> { + this.pluginService.getSessionStateService().setupPristineNetworkTimeout(); this.pluginService.getCurrentConnection().setNetworkTimeout(executor, milliseconds); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.NETWORK_TIMEOUT); + this.pluginService.getSessionStateService().setNetworkTimeout(milliseconds); }, executor, milliseconds); @@ -837,8 +847,9 @@ public void setSchema(final String schema) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setSchema", () -> { + this.pluginService.getSessionStateService().setupPristineSchema(); this.pluginService.getCurrentConnection().setSchema(schema); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.SCHEMA); + this.pluginService.getSessionStateService().setSchema(schema); }, schema); } @@ -851,8 +862,9 @@ public void setTransactionIsolation(final int level) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setTransactionIsolation", () -> { + this.pluginService.getSessionStateService().setupPristineTransactionIsolation(); this.pluginService.getCurrentConnection().setTransactionIsolation(level); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.TRANSACTION_ISOLATION); + this.pluginService.getSessionStateService().setTransactionIsolation(level); }, level); } @@ -865,8 +877,9 @@ public void setTypeMap(final Map> map) throws SQLException { this.pluginService.getCurrentConnection(), "Connection.setTypeMap", () -> { + this.pluginService.getSessionStateService().setupPristineTypeMap(); this.pluginService.getCurrentConnection().setTypeMap(map); - this.pluginService.setCurrentConnectionState(SessionDirtyFlag.TYPE_MAP); + this.pluginService.getSessionStateService().setTypeMap(map); }, map); } 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 627aa1334..69fe2cb6e 100644 --- a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties +++ b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties @@ -14,12 +14,21 @@ # limitations under the License. # +# ADFS Credentials Provider Getter +AdfsCredentialsProviderFactory.failedLogin=Failed login. Could not obtain SAML Assertion from ADFS SignOn Page POST response: \n''{0}'' +AdfsCredentialsProviderFactory.getSamlAssertionFailed=Failed to get SAML Assertion due to exception: ''{0}'' +AdfsCredentialsProviderFactory.invalidHttpsUrl=Invalid HTTPS URL: ''{0}'' +AdfsCredentialsProviderFactory.signOnPagePostActionUrl=ADFS SignOn Action URL: ''{0}'' +AdfsCredentialsProviderFactory.signOnPagePostActionRequestFailed=ADFS SignOn Page POST action failed with HTTP status ''{0}'', reason phrase ''{1}'', and response ''{2}'' +AdfsCredentialsProviderFactory.signOnPageRequestFailed=ADFS SignOn Page Request Failed with HTTP status ''{0}'', reason phrase ''{1}'', and response ''{2}'' +AdfsCredentialsProviderFactory.signOnPageUrl=ADFS SignOn URL: ''{0}'' + # Aurora Host List Connection Plugin AuroraHostListConnectionPlugin.providerAlreadySet=Another dynamic host list provider has already been set: {0}. # Aurora Host List Provider -RdsHostListProvider.clusterInstanceHostPatternRequired=The ''clusterInstanceHostPattern'' configuration property is required when an IP address or custom domain is used to connect to a cluster that provides topology information. If you would instead like to connect without failover functionality, set the 'enableClusterAwareFailover' configuration property to false. 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}''. @@ -47,7 +56,7 @@ AwsSecretsManagerConnectionPlugin.unhandledException=Unhandled exception: ''{0}' # AWS Wrapper Data Source AwsWrapperDataSource.missingJdbcProtocol=Missing JDBC protocol. Could not construct URL. AwsWrapperDataSource.missingTarget=JDBC url or Server name is required. -AwsWrapperDataSource.missingDriver=Can't find a suitable driver for ''{0}'' +AwsWrapperDataSource.configurationProfileNotFound=Configuration profile ''{0}'' not found. # Cluster Aware Reader Failover Handler ClusterAwareReaderFailoverHandler.interruptedThread=Thread was interrupted. @@ -81,7 +90,6 @@ ConnectionStringHostListProvider.parsedListEmpty=Can''t parse connection string: ConnectionStringHostListProvider.errorIdentifyConnection=An error occurred while obtaining the connection's host ID. # Connection Plugin Manager -ConnectionPluginManager.configurationProfileNotFound=Configuration profile ''{0}'' not found. ConnectionPluginManager.releaseResources=Releasing resources. ConnectionPluginManager.unknownPluginCode=Unknown plugin code: ''{0}''. ConnectionPluginManager.unableToLoadPlugin=Unable to load connection plugin factory: ''{0}''. @@ -95,11 +103,13 @@ ConnectionProvider.unsupportedHostSpecSelectorStrategy=Unsupported host selectio ConnectionUrlBuilder.missingJdbcProtocol=Missing JDBC protocol and/or host name. Could not construct URL. ConnectionUrlBuilder.failureEncodingConnectionUrl=Failed to encode connectionURL properties. +# Connection Url Parser +ConnectionUrlParser.protocolNotFound=Url should contain a driver protocol. Protocol is not found in url: ''{0}'' + # Connect Time Connection Plugin ConnectTimeConnectionPlugin.connectTime=Connected in {0} nanos. # Connection Wrapper -ConnectionWrapper.protocolNotFound=Url should contain a driver protocol. Protocol is not found in url: ''{0}'' ConnectionWrapper.unclosedConnectionInstantiated=Unclosed connection was instantiated at this point: ConnectionWrapper.connectionNotOpen=Initial connection isn't open. ConnectionWrapper.finalizingUnclosedConnection=Finalizing a connection that was never closed. @@ -121,6 +131,7 @@ Driver.alreadyRegistered=Driver is already registered. It can only be registered 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} Driver.notRegistered=Driver is not registered (or it has not been registered using Driver.register() method). Driver.urlParsingFailed=Url [{0}] parsing failed with error: [{1}] +Driver.configurationProfileNotFound=Configuration profile ''{0}'' not found. # DataSource DataSource.failedToSetProperty=Failed to set property ''{0}'' on target datasource ''{1}''. @@ -146,6 +157,16 @@ Failover.failedToUpdateCurrentHostspecAvailability=Failed to update current host Failover.noOperationsAfterConnectionClosed=No operations allowed after connection closed. Failover.invalidHostListProvider=Incorrect type of host list provider found, please ensure the correct host list provider is specified. The host list provider in use is: ''{0}'', the plugin is expected a cluster-aware host list provider such as the AuroraHostListProvider. +# Federated Authentication Connection Plugin +FederatedAuthPlugin.generatedNewIamToken=Generated new IAM token = ''{0}'' +FederatedAuthPlugin.javaStsSdkNotInClasspath=Required dependency 'AWS Java SDK for AWS Secret Token Service' is not on the classpath. +FederatedAuthPlugin.unhandledException=Unhandled exception: ''{0}'' +FederatedAuthPlugin.unsupportedHostname=Unsupported AWS hostname {0}. Amazon domain name in format *.AWS-Region.rds.amazonaws.com or *.rds.AWS-Region.amazonaws.com.cn is expected. +FederatedAuthPlugin.useCachedIamToken=Use cached IAM token = ''{0}'' + +# Federated Authentication Connection Plugin Factory +FederatedAuthPluginFactory.failedToInitializeHttpClient=Failed to initialize HttpClient. +FederatedAuthPluginFactory.unsupportedIdp=Unsupported Identity Provider ''{0}''. Please visit to the documentation for supported Identity Providers. # HikariPooledConnectionProvider HikariPooledConnectionProvider.errorConnectingWithDataSource=Unable to connect to ''{0}'' using the Hikari data source. @@ -171,7 +192,6 @@ HostSelector.roundRobinInvalidDefaultWeight=The provided default weight value is IamAuthConnectionPlugin.unsupportedHostname=Unsupported AWS hostname {0}. Amazon domain name in format *.AWS-Region.rds.amazonaws.com or *.rds.AWS-Region.amazonaws.com.cn is expected. IamAuthConnectionPlugin.useCachedIamToken=Use cached IAM token = ''{0}'' IamAuthConnectionPlugin.generatedNewIamToken=Generated new IAM token = ''{0}'' -IamAuthConnectionPlugin.invalidPort=Port number: {0} is not valid. Port number should be greater than zero. Falling back to default port. IamAuthConnectionPlugin.unhandledException=Unhandled exception: ''{0}'' IamAuthConnectionPlugin.connectException=Error occurred while opening a connection: ''{0}'' @@ -193,6 +213,7 @@ MonitorImpl.interruptedExceptionDuringMonitoring=Monitoring thread for node {0} MonitorImpl.exceptionDuringMonitoringContinue=Continuing monitoring after unhandled exception was thrown in monitoring thread for node {0}. MonitorImpl.exceptionDuringMonitoringStop=Stopping monitoring after unhandled exception was thrown in monitoring thread for node {0}. MonitorImpl.monitorIsStopped=Monitoring was already stopped for node {0}. +MonitorImpl.stopped=Stopped monitoring thread for node ''{0}''. # Monitor Service Impl MonitorServiceImpl.emptyAliasSet=Empty alias set passed for ''{0}''. Set should not be empty. @@ -263,8 +284,22 @@ DialectManager.unknownDialect=Database dialect can''t be identified. Use configu # Target Driver Dialect Manager TargetDriverDialectManager.unknownDialectCode=Unknown target driver dialect code: ''{0}''. +TargetDriverDialectManager.unknownProtocol=Can not find a driver to register for protocol ''{0}''. TargetDriverDialectManager.customDialectNotSupported=Provided custom target driver dialect will be ignored. TargetDriverDialectManager.useDialect=Target driver dialect set to: ''{0}'', {1}. TargetDriverDialectManager.unexpectedClass=Unexpected DataSource class. Expected class: {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. +MariadbDriverHelper.canNotRegister=Can''t register driver org.mariadb.jdbc.Driver. + +# Aurora Initial Connection Strategy Plugin +AuroraInitialConnectionStrategyPlugin.unsupportedStrategy=Unsupported host selection strategy ''{0}''. +AuroraInitialConnectionStrategyPlugin.requireDynamicProvider=Dynamic host list provider is required. + +# Fastest Response Time Strategy Plugin +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}. +NodeResponseTimeMonitor.openingConnection=Opening a Response time connection to ''{0}''. +NodeResponseTimeMonitor.openedConnection=Opened Response time connection: {0}. diff --git a/wrapper/src/test/build.gradle.kts b/wrapper/src/test/build.gradle.kts index b52eecc92..29b27b157 100644 --- a/wrapper/src/test/build.gradle.kts +++ b/wrapper/src/test/build.gradle.kts @@ -41,8 +41,9 @@ dependencies { testImplementation("com.zaxxer:HikariCP:4.+") // version 4.+ is compatible with Java 8 testImplementation("org.springframework.boot:spring-boot-starter-jdbc:2.7.13") // 2.7.13 is the last version compatible with Java 8 testImplementation("org.mockito:mockito-inline:4.11.0") // 4.11.0 is the last version compatible with Java 8 - testImplementation("software.amazon.awssdk:rds:2.20.49") testImplementation("software.amazon.awssdk:ec2:2.20.49") + testImplementation("software.amazon.awssdk:rds:2.20.49") + testImplementation("software.amazon.awssdk:sts:2.20.49") testImplementation("org.testcontainers:testcontainers:1.17.+") testImplementation("org.testcontainers:mysql:1.17.+") testImplementation("org.testcontainers:postgresql:1.17.+") @@ -64,7 +65,16 @@ tasks.withType { classpath += fileTree("./libs") { include("*.jar") } + project.files("./test") outputs.upToDateWhen { false } - useJUnitPlatform() + useJUnitPlatform { + System.getProperty("test-include-tags")?.split(",")?.forEach { tag -> + includeTags(tag) + println("Include tests with tag: $tag") + } + System.getProperty("test-exclude-tags")?.split(",")?.forEach { tag -> + excludeTags(tag) + println("Exclude tests with tag: $tag") + } + } testLogging { events(PASSED, FAILED, SKIPPED) diff --git a/wrapper/src/test/java/integration/container/ConnectionStringHelper.java b/wrapper/src/test/java/integration/container/ConnectionStringHelper.java index 3ac06428b..f5b56355e 100644 --- a/wrapper/src/test/java/integration/container/ConnectionStringHelper.java +++ b/wrapper/src/test/java/integration/container/ConnectionStringHelper.java @@ -204,7 +204,7 @@ public static Properties getDefaultProperties() { PropertyDefinition.TELEMETRY_METRICS_BACKEND.name, features.contains(TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED) ? "otlp" : "none"); - DriverHelper.setTcpKeepAlive(TestEnvironment.getCurrent().getCurrentDriver(), props, false); + props.setProperty(PropertyDefinition.TCP_KEEP_ALIVE.name, "false"); return props; } diff --git a/wrapper/src/test/java/integration/container/TestDriverProvider.java b/wrapper/src/test/java/integration/container/TestDriverProvider.java index cd4057d8a..63fabeea2 100644 --- a/wrapper/src/test/java/integration/container/TestDriverProvider.java +++ b/wrapper/src/test/java/integration/container/TestDriverProvider.java @@ -54,11 +54,15 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.platform.commons.util.AnnotationUtils; import software.amazon.jdbc.dialect.DialectManager; +import software.amazon.jdbc.plugin.efm.MonitorThreadContainer; +import software.amazon.jdbc.plugin.efm2.MonitorServiceImpl; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialectManager; public class TestDriverProvider implements TestTemplateInvocationContextProvider { private static final Logger LOGGER = Logger.getLogger(TestDriverProvider.class.getName()); + private static final String POSTGRES_AUTH_ERROR_CODE = "28P01"; + @Override public boolean supportsTestTemplate(ExtensionContext context) { return true; @@ -165,6 +169,10 @@ public void beforeEach(ExtensionContext context) throws Exception { try { instanceIDs = auroraUtil.getAuroraInstanceIds(); } catch (SQLException ex) { + if (POSTGRES_AUTH_ERROR_CODE.equals(ex.getSQLState())) { + // This authentication error for PG is caused by test environment configuration. + throw ex; + } instanceIDs = new ArrayList<>(); } } @@ -206,6 +214,8 @@ public void beforeEach(ExtensionContext context) throws Exception { TestPluginServiceImpl.clearHostAvailabilityCache(); DialectManager.resetEndpointCache(); TargetDriverDialectManager.resetCustomDialect(); + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); } if (tracesEnabled) { AWSXRay.endSegment(); diff --git a/wrapper/src/test/java/integration/container/aurora/TestPluginServiceImpl.java b/wrapper/src/test/java/integration/container/aurora/TestPluginServiceImpl.java index ab1cc7996..4a54ee984 100644 --- a/wrapper/src/test/java/integration/container/aurora/TestPluginServiceImpl.java +++ b/wrapper/src/test/java/integration/container/aurora/TestPluginServiceImpl.java @@ -21,14 +21,19 @@ import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.ConnectionPluginManager; import software.amazon.jdbc.PluginServiceImpl; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; public class TestPluginServiceImpl extends PluginServiceImpl { public TestPluginServiceImpl( @NonNull ConnectionPluginManager pluginManager, @NonNull Properties props, - @NonNull String originalUrl, String targetDriverProtocol) throws SQLException { - super(pluginManager, props, originalUrl, targetDriverProtocol); + @NonNull String originalUrl, + String targetDriverProtocol, + @NonNull final TargetDriverDialect targetDriverDialect) + throws SQLException { + + super(pluginManager, props, originalUrl, targetDriverProtocol, targetDriverDialect); } public static void clearHostAvailabilityCache() { diff --git a/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java b/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java index 57320801e..2b4a1ba5b 100644 --- a/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java +++ b/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java @@ -18,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static software.amazon.jdbc.PropertyDefinition.CONNECT_TIMEOUT; import static software.amazon.jdbc.PropertyDefinition.PLUGINS; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME; import static software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin.FAILOVER_TIMEOUT_MS; -import integration.DriverHelper; import integration.TestEnvironmentFeatures; import integration.container.ConnectionStringHelper; import integration.container.TestDriverProvider; @@ -58,10 +58,14 @@ import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.plugin.efm.MonitorThreadContainer; +import software.amazon.jdbc.plugin.efm2.MonitorServiceImpl; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; import software.amazon.jdbc.util.StringUtils; @@ -71,10 +75,13 @@ TestEnvironmentFeatures.PERFORMANCE, TestEnvironmentFeatures.FAILOVER_SUPPORTED }) +@Tag("advanced") public class AdvancedPerformanceTest { private static final Logger LOGGER = Logger.getLogger(AdvancedPerformanceTest.class.getName()); + private static final String MONITORING_CONNECTION_PREFIX = "monitoring-"; + private static final int REPEAT_TIMES = StringUtils.isNullOrEmpty(System.getenv("REPEAT_TIMES")) ? 5 @@ -178,9 +185,11 @@ public void test_AdvancedPerformance() throws IOException { } finally { doWritePerfDataToFile( String.format( - "./build/reports/tests/DbEngine_%s_Driver_%s_AdvancedPerformanceResults.xlsx", + "./build/reports/tests/AdvancedPerformanceResults_" + + "Db_%s_Driver_%s_Instances_%d.xlsx", TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine(), - TestEnvironment.getCurrent().getCurrentDriver()), + TestEnvironment.getCurrent().getCurrentDriver(), + TestEnvironment.getCurrent().getInfo().getRequest().getNumOfInstances()), perfDataList); perfDataList.clear(); } @@ -188,21 +197,21 @@ public void test_AdvancedPerformance() throws IOException { private void doMeasurePerformance(int sleepDelayMillis) throws InterruptedException { - final AtomicLong downtime = new AtomicLong(); + final AtomicLong downtimeNano = new AtomicLong(); final CountDownLatch startLatch = new CountDownLatch(5); final CountDownLatch finishLatch = new CountDownLatch(5); - downtime.set(0); + downtimeNano.set(0); final Thread failoverThread = - getThread_Failover(sleepDelayMillis, downtime, startLatch, finishLatch); + getThread_Failover(sleepDelayMillis, downtimeNano, startLatch, finishLatch); final Thread pgThread = - getThread_DirectDriver(sleepDelayMillis, downtime, startLatch, finishLatch); + getThread_DirectDriver(sleepDelayMillis, downtimeNano, startLatch, finishLatch); final Thread wrapperEfmThread = - getThread_WrapperEfm(sleepDelayMillis, downtime, startLatch, finishLatch); + getThread_WrapperEfm(sleepDelayMillis, downtimeNano, startLatch, finishLatch); final Thread wrapperEfmFailoverThread = - getThread_WrapperEfmFailover(sleepDelayMillis, downtime, startLatch, finishLatch); - final Thread dnsThread = getThread_DNS(sleepDelayMillis, downtime, startLatch, finishLatch); + getThread_WrapperEfmFailover(sleepDelayMillis, downtimeNano, startLatch, finishLatch); + final Thread dnsThread = getThread_DNS(sleepDelayMillis, downtimeNano, startLatch, finishLatch); failoverThread.start(); pgThread.start(); @@ -216,6 +225,8 @@ private void doMeasurePerformance(int sleepDelayMillis) throws InterruptedExcept LOGGER.finest("Test is over."); + assertTrue(downtimeNano.get() > 0); + failoverThread.interrupt(); pgThread.interrupt(); wrapperEfmThread.interrupt(); @@ -269,7 +280,7 @@ private void ensureDnsHealthy() throws UnknownHostException, InterruptedExceptio private Thread getThread_Failover( final int sleepDelayMillis, - final AtomicLong downtime, + final AtomicLong downtimeNano, final CountDownLatch startLatch, final CountDownLatch finishLatch) { @@ -286,8 +297,8 @@ private Thread getThread_Failover( LOGGER.finest("Trigger failover..."); // trigger failover - auroraUtil.failoverClusterAndWaitUntilWriterChanged(); - downtime.set(System.nanoTime()); + failoverCluster(); + downtimeNano.set(System.nanoTime()); LOGGER.finest("Failover is started."); } catch (InterruptedException interruptedException) { @@ -303,13 +314,13 @@ private Thread getThread_Failover( private Thread getThread_DirectDriver( final int sleepDelayMillis, - final AtomicLong downtime, + final AtomicLong downtimeNano, final CountDownLatch startLatch, final CountDownLatch finishLatch) { return new Thread( () -> { - long failureTime = 0; + long failureTimeNano = 0; try { // DB_CONN_STR_PREFIX final Properties props = ConnectionStringHelper.getDefaultProperties(); @@ -345,7 +356,8 @@ private Thread getThread_DirectDriver( } catch (SQLException throwable) { // Catching executing query LOGGER.finest("DirectDriver thread exception: " + throwable); // Calculate and add detection time - failureTime = (System.nanoTime() - downtime.get()); + assertTrue(downtimeNano.get() > 0); + failureTimeNano = System.nanoTime() - downtimeNano.get(); } } catch (InterruptedException interruptedException) { @@ -357,7 +369,8 @@ private Thread getThread_DirectDriver( data.paramFailoverDelayMillis = sleepDelayMillis; data.paramDriverName = "DirectDriver - " + TestEnvironment.getCurrent().getCurrentDriver(); - data.failureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTime); + data.failureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTimeNano); + LOGGER.finest("DirectDriver Collected data: " + data); perfDataList.add(data); LOGGER.finest( "DirectDriver Failure detection time is " + data.failureDetectionTimeMillis + "ms"); @@ -370,18 +383,24 @@ private Thread getThread_DirectDriver( private Thread getThread_WrapperEfm( final int sleepDelayMillis, - final AtomicLong downtime, + final AtomicLong downtimeNano, final CountDownLatch startLatch, final CountDownLatch finishLatch) { return new Thread( () -> { - long failureTime = 0; + long failureTimeNano = 0; try { final Properties props = ConnectionStringHelper.getDefaultProperties(); - DriverHelper.setMonitoringConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setMonitoringSocketTimeout(props, TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); + + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(TIMEOUT_SEC))); + CONNECT_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + FAILURE_DETECTION_TIME.set(props, Integer.toString(EFM_FAILURE_DETECTION_TIME_MS)); FAILURE_DETECTION_INTERVAL.set(props, Integer.toString(EFM_FAILURE_DETECTION_INTERVAL_MS)); FAILURE_DETECTION_COUNT.set(props, Integer.toString(EFM_FAILURE_DETECTION_COUNT)); @@ -419,7 +438,8 @@ private Thread getThread_WrapperEfm( LOGGER.finest("WrapperEfm thread exception: " + throwable); // Calculate and add detection time - failureTime = (System.nanoTime() - downtime.get()); + assertTrue(downtimeNano.get() > 0); + failureTimeNano = System.nanoTime() - downtimeNano.get(); } } catch (InterruptedException interruptedException) { @@ -432,7 +452,8 @@ private Thread getThread_WrapperEfm( data.paramDriverName = String.format( "AWS Wrapper (%s, EFM)", TestEnvironment.getCurrent().getCurrentDriver()); - data.failureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTime); + data.failureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTimeNano); + LOGGER.finest("WrapperEfm Collected data: " + data); perfDataList.add(data); LOGGER.finest( "WrapperEfm Failure detection time is " + data.failureDetectionTimeMillis + "ms"); @@ -445,18 +466,24 @@ private Thread getThread_WrapperEfm( private Thread getThread_WrapperEfmFailover( final int sleepDelayMillis, - final AtomicLong downtime, + final AtomicLong downtimeNano, final CountDownLatch startLatch, final CountDownLatch finishLatch) { return new Thread( () -> { - long failureTime = 0; + long failureTimeNano = 0; try { final Properties props = ConnectionStringHelper.getDefaultProperties(); - DriverHelper.setMonitoringConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setMonitoringSocketTimeout(props, TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); + + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(TIMEOUT_SEC))); + CONNECT_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + FAILURE_DETECTION_TIME.set(props, Integer.toString(EFM_FAILURE_DETECTION_TIME_MS)); FAILURE_DETECTION_INTERVAL.set(props, Integer.toString(EFM_FAILURE_DETECTION_TIME_MS)); FAILURE_DETECTION_COUNT.set(props, Integer.toString(EFM_FAILURE_DETECTION_COUNT)); @@ -495,7 +522,8 @@ private Thread getThread_WrapperEfmFailover( LOGGER.finest("WrapperEfmFailover thread exception: " + throwable); if (throwable instanceof FailoverSuccessSQLException) { // Calculate and add detection time - failureTime = (System.nanoTime() - downtime.get()); + assertTrue(downtimeNano.get() > 0); + failureTimeNano = System.nanoTime() - downtimeNano.get(); } } @@ -510,7 +538,8 @@ private Thread getThread_WrapperEfmFailover( String.format( "AWS Wrapper (%s, EFM, Failover)", TestEnvironment.getCurrent().getCurrentDriver()); - data.reconnectTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTime); + data.reconnectTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTimeNano); + LOGGER.finest("WrapperEfmFailover Collected data: " + data); perfDataList.add(data); LOGGER.finest( "WrapperEfmFailover Reconnect time is " + data.reconnectTimeMillis + "ms"); @@ -523,13 +552,13 @@ private Thread getThread_WrapperEfmFailover( private Thread getThread_DNS( final int sleepDelayMillis, - final AtomicLong downtime, + final AtomicLong downtimeNano, final CountDownLatch startLatch, final CountDownLatch finishLatch) { return new Thread( () -> { - long failureTime = 0; + long failureTimeNano = 0; String currentClusterIpAddress; try { @@ -571,7 +600,8 @@ private Thread getThread_DNS( // DNS data has changed if (!clusterIpAddress.equals(currentClusterIpAddress)) { - failureTime = (System.nanoTime() - downtime.get()); + assertTrue(downtimeNano.get() > 0); + failureTimeNano = System.nanoTime() - downtimeNano.get(); } } catch (InterruptedException interruptedException) { @@ -582,7 +612,8 @@ private Thread getThread_DNS( PerfStat data = new PerfStat(); data.paramFailoverDelayMillis = sleepDelayMillis; data.paramDriverName = "DNS"; - data.dnsUpdateTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTime); + data.dnsUpdateTimeMillis = TimeUnit.NANOSECONDS.toMillis(failureTimeNano); + LOGGER.finest("DNS Collected data: " + data); perfDataList.add(data); LOGGER.finest("DNS Update time is " + data.dnsUpdateTimeMillis + "ms"); @@ -611,6 +642,12 @@ private Connection openConnectionWithRetry(String url, Properties props) { return conn; } + private void failoverCluster() throws InterruptedException { + String clusterId = TestEnvironment.getCurrent().getInfo().getAuroraClusterName(); + String randomNode = auroraUtil.getRandomDBClusterReaderInstanceId(clusterId); + auroraUtil.failoverClusterToTarget(clusterId, randomNode); + } + private void ensureClusterHealthy() throws InterruptedException { auroraUtil.waitUntilClusterHasRightState( @@ -649,6 +686,8 @@ private void ensureClusterHealthy() throws InterruptedException { TestAuroraHostListProvider.clearCache(); TestPluginServiceImpl.clearHostAvailabilityCache(); + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); } private static Stream generateParams() { @@ -710,5 +749,17 @@ public void writeData(Row row) { cell = row.createCell(4); cell.setCellValue(this.dnsUpdateTimeMillis); } + + @Override + public String toString() { + return String.format("%s [\nparamDriverName=%s,\nparamFailoverDelayMillis=%d,\n" + + "failureDetectionTimeMillis=%d,\nreconnectTimeMillis=%d,\ndnsUpdateTimeMillis=%d ]", + super.toString(), + this.paramDriverName, + this.paramFailoverDelayMillis, + this.failureDetectionTimeMillis, + this.reconnectTimeMillis, + this.dnsUpdateTimeMillis); + } } } diff --git a/wrapper/src/test/java/integration/container/tests/DriverConfigurationProfileTests.java b/wrapper/src/test/java/integration/container/tests/DriverConfigurationProfileTests.java index 9af73bf89..c843a6633 100644 --- a/wrapper/src/test/java/integration/container/tests/DriverConfigurationProfileTests.java +++ b/wrapper/src/test/java/integration/container/tests/DriverConfigurationProfileTests.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.plugin.ExecutionTimeConnectionPluginFactory; +import software.amazon.jdbc.profile.ConfigurationProfileBuilder; import software.amazon.jdbc.profile.DriverConfigurationProfiles; import software.amazon.jdbc.wrapper.ConnectionWrapper; import software.amazon.jdbc.wrapper.ResultSetWrapper; @@ -74,8 +75,10 @@ public void testOpenConnectionWithProfile() throws SQLException { props.setProperty(PropertyDefinition.PROFILE_NAME.name, "testProfile"); DriverConfigurationProfiles.clear(); - DriverConfigurationProfiles.addOrReplaceProfile( - "testProfile", Collections.singletonList(ExecutionTimeConnectionPluginFactory.class)); + ConfigurationProfileBuilder.get() + .withName("testProfile") + .withPluginFactories(Collections.singletonList(ExecutionTimeConnectionPluginFactory.class)) + .buildAndSet(); Connection conn = DriverManager.getConnection(ConnectionStringHelper.getWrapperUrl(), props); diff --git a/wrapper/src/test/java/integration/container/tests/PerformanceTest.java b/wrapper/src/test/java/integration/container/tests/PerformanceTest.java index 13e3846d5..fa5aa19e0 100644 --- a/wrapper/src/test/java/integration/container/tests/PerformanceTest.java +++ b/wrapper/src/test/java/integration/container/tests/PerformanceTest.java @@ -17,13 +17,15 @@ package integration.container.tests; import static org.junit.jupiter.api.Assertions.fail; +import static software.amazon.jdbc.PropertyDefinition.CONNECT_TIMEOUT; import static software.amazon.jdbc.PropertyDefinition.PLUGINS; +import static software.amazon.jdbc.PropertyDefinition.SOCKET_TIMEOUT; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_COUNT; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_INTERVAL; import static software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin.FAILURE_DETECTION_TIME; import static software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin.FAILOVER_TIMEOUT_MS; -import integration.DriverHelper; +import integration.DatabaseEngine; import integration.TestEnvironmentFeatures; import integration.container.ConnectionStringHelper; import integration.container.ProxyHelper; @@ -40,6 +42,7 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.LongSummaryStatistics; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -50,10 +53,14 @@ import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.plugin.efm.MonitorThreadContainer; +import software.amazon.jdbc.plugin.efm2.MonitorServiceImpl; import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin; import software.amazon.jdbc.util.StringUtils; @@ -68,6 +75,8 @@ public class PerformanceTest { private static final Logger LOGGER = Logger.getLogger(PerformanceTest.class.getName()); + private static final String MONITORING_CONNECTION_PREFIX = "monitoring-"; + private static final int REPEAT_TIMES = StringUtils.isNullOrEmpty(System.getenv("REPEAT_TIMES")) ? 5 @@ -120,8 +129,21 @@ private void doWritePerfDataToFile(String fileName, List } @TestTemplate + @Tag("efm") public void test_FailureDetectionTime_EnhancedMonitoringEnabled() throws IOException { + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); + test_FailureDetectionTime_EnhancedMonitoringEnabled("efm"); + + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); + test_FailureDetectionTime_EnhancedMonitoringEnabled("efm2"); + } + + public void test_FailureDetectionTime_EnhancedMonitoringEnabled(final String efmPlugin) + throws IOException { + enhancedFailureMonitoringPerfDataList.clear(); try { @@ -131,7 +153,7 @@ public void test_FailureDetectionTime_EnhancedMonitoringEnabled() throws IOExcep try { Object[] args = a.get(); execute_FailureDetectionTime_EnhancedMonitoringEnabled( - (int) args[0], (int) args[1], (int) args[2], (int) args[3]); + efmPlugin, (int) args[0], (int) args[1], (int) args[2], (int) args[3]); } catch (SQLException ex) { throw new RuntimeException(ex); } @@ -140,39 +162,63 @@ public void test_FailureDetectionTime_EnhancedMonitoringEnabled() throws IOExcep } finally { doWritePerfDataToFile( String.format( - "./build/reports/tests/" - + "DbEngine_%s_Driver_%s_" - + "FailureDetectionPerformanceResults_EnhancedMonitoringEnabled.xlsx", + "./build/reports/tests/EnhancedMonitoringOnly_" + + "Db_%s_Driver_%s_Instances_%d_Plugin_%s.xlsx", TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine(), - TestEnvironment.getCurrent().getCurrentDriver()), + TestEnvironment.getCurrent().getCurrentDriver(), + TestEnvironment.getCurrent().getInfo().getRequest().getNumOfInstances(), + efmPlugin), enhancedFailureMonitoringPerfDataList); enhancedFailureMonitoringPerfDataList.clear(); } } private void execute_FailureDetectionTime_EnhancedMonitoringEnabled( - int detectionTime, int detectionInterval, int detectionCount, int sleepDelayMillis) + final String efmPlugin, + int detectionTimeMillis, + int detectionIntervalMillis, + int detectionCount, + int sleepDelayMillis) throws SQLException { + final Properties props = ConnectionStringHelper.getDefaultProperties(); - DriverHelper.setMonitoringConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setMonitoringSocketTimeout(props, TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(TIMEOUT_SEC))); + CONNECT_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + // this performance test measures efm failure detection time after disconnecting the network - FAILURE_DETECTION_TIME.set(props, Integer.toString(detectionTime)); - FAILURE_DETECTION_INTERVAL.set(props, Integer.toString(detectionInterval)); + FAILURE_DETECTION_TIME.set(props, Integer.toString(detectionTimeMillis)); + FAILURE_DETECTION_INTERVAL.set(props, Integer.toString(detectionIntervalMillis)); FAILURE_DETECTION_COUNT.set(props, Integer.toString(detectionCount)); - PLUGINS.set(props, "efm"); + PLUGINS.set(props, efmPlugin); final PerfStatMonitoring data = new PerfStatMonitoring(); doMeasurePerformance(sleepDelayMillis, REPEAT_TIMES, props, data); - data.paramDetectionTime = detectionTime; - data.paramDetectionInterval = detectionInterval; + data.paramDetectionTime = detectionTimeMillis; + data.paramDetectionInterval = detectionIntervalMillis; data.paramDetectionCount = detectionCount; enhancedFailureMonitoringPerfDataList.add(data); } @TestTemplate + @Tag("efm") + @Tag("failover") public void test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled() throws IOException { + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); + test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled("efm"); + + MonitorThreadContainer.releaseInstance(); + MonitorServiceImpl.clearCache(); + test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled("efm2"); + } + + public void test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled(final String efmPlugin) + throws IOException { failoverWithEfmPerfDataList.clear(); @@ -183,7 +229,7 @@ public void test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled() thr try { Object[] args = a.get(); execute_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled( - (int) args[0], (int) args[1], (int) args[2], (int) args[3]); + efmPlugin, (int) args[0], (int) args[1], (int) args[2], (int) args[3]); } catch (SQLException ex) { throw new RuntimeException(ex); } @@ -192,31 +238,40 @@ public void test_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled() thr } finally { doWritePerfDataToFile( String.format( - "./build/reports/tests/" - + "DbEngine_%s_Driver_%s_" - + "FailureDetectionPerformanceResults_FailoverAndEnhancedMonitoringEnabled.xlsx", + "./build/reports/tests/FailoverWithEnhancedMonitoring_" + + "Db_%s_Driver_%s_Instances_%d_Plugin_%s.xlsx", TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine(), - TestEnvironment.getCurrent().getCurrentDriver()), + TestEnvironment.getCurrent().getCurrentDriver(), + TestEnvironment.getCurrent().getInfo().getRequest().getNumOfInstances(), + efmPlugin), failoverWithEfmPerfDataList); failoverWithEfmPerfDataList.clear(); } } private void execute_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled( - int detectionTime, int detectionInterval, int detectionCount, int sleepDelayMillis) + final String efmPlugin, + int detectionTime, + int detectionInterval, + int detectionCount, + int sleepDelayMillis) throws SQLException { final Properties props = ConnectionStringHelper.getDefaultProperties(); - DriverHelper.setMonitoringConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setMonitoringSocketTimeout(props, TIMEOUT_SEC, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.CONNECT_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); + props.setProperty( + MONITORING_CONNECTION_PREFIX + PropertyDefinition.SOCKET_TIMEOUT.name, + String.valueOf(TimeUnit.SECONDS.toMillis(TIMEOUT_SEC))); + CONNECT_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); // this performance test measures failover and efm failure detection time after disconnecting // the network FAILURE_DETECTION_TIME.set(props, Integer.toString(detectionTime)); FAILURE_DETECTION_INTERVAL.set(props, Integer.toString(detectionInterval)); FAILURE_DETECTION_COUNT.set(props, Integer.toString(detectionCount)); - PLUGINS.set(props, "failover,efm"); + PLUGINS.set(props, "failover," + efmPlugin); FAILOVER_TIMEOUT_MS.set(props, Integer.toString(PERF_FAILOVER_TIMEOUT_MS)); props.setProperty( "clusterInstanceHostPattern", @@ -236,6 +291,7 @@ private void execute_FailureDetectionTime_FailoverAndEnhancedMonitoringEnabled( } @TestTemplate + @Tag("failover") public void test_FailoverTime_SocketTimeout() throws IOException { failoverWithSocketTimeoutPerfDataList.clear(); @@ -255,9 +311,11 @@ public void test_FailoverTime_SocketTimeout() throws IOException { } finally { doWritePerfDataToFile( String.format( - "./build/reports/tests/DbEngine_%s_Driver_%s_FailoverPerformanceResults_SocketTimeout.xlsx", + "./build/reports/tests/FailoverWithSocketTimeout_" + + "Db_%s_Driver_%s_Instances_%d.xlsx", TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine(), - TestEnvironment.getCurrent().getCurrentDriver()), + TestEnvironment.getCurrent().getCurrentDriver(), + TestEnvironment.getCurrent().getInfo().getRequest().getNumOfInstances()), failoverWithSocketTimeoutPerfDataList); failoverWithSocketTimeoutPerfDataList.clear(); } @@ -268,8 +326,8 @@ private void execute_FailoverTime_SocketTimeout(int socketTimeout, int sleepDela final Properties props = ConnectionStringHelper.getDefaultProperties(); // this performance test measures how socket timeout changes the overall failover time - DriverHelper.setSocketTimeout(props, socketTimeout, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS); + SOCKET_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(socketTimeout))); + CONNECT_TIMEOUT.set(props, String.valueOf(TimeUnit.SECONDS.toMillis(CONNECT_TIMEOUT_SEC))); // Loads just failover plugin; don't load Enhanced Failure Monitoring plugin props.setProperty("wrapperPlugins", "failover"); @@ -296,12 +354,11 @@ private void doMeasurePerformance( PerfStatBase data) throws SQLException { - final String QUERY = "SELECT pg_sleep(600)"; // 600s -> 10min - final AtomicLong downtime = new AtomicLong(); - final List elapsedTimes = new ArrayList<>(repeatTimes); + final AtomicLong downtimeNanos = new AtomicLong(); + final List elapsedTimeMillis = new ArrayList<>(repeatTimes); for (int i = 0; i < repeatTimes; i++) { - downtime.set(0); + downtimeNanos.set(0); // Thread to stop network final Thread thread = @@ -317,7 +374,8 @@ private void doMeasurePerformance( .getInstances() .get(0) .getInstanceId()); - downtime.set(System.nanoTime()); + downtimeNanos.set(System.nanoTime()); + LOGGER.finest("Network outages started."); } catch (InterruptedException interruptedException) { // Ignore, stop the thread } @@ -329,12 +387,16 @@ private void doMeasurePerformance( thread.start(); // Execute long query - try (final ResultSet result = statement.executeQuery(QUERY)) { + try (final ResultSet result = statement.executeQuery(getQuerySql())) { fail("Sleep query finished, should not be possible with network downed."); } catch (SQLException ex) { // Catching executing query // Calculate and add detection time - final long failureTime = (System.nanoTime() - downtime.get()); - elapsedTimes.add(failureTime); + if (downtimeNanos.get() == 0) { + LOGGER.warning("Network outages start time is undefined!"); + } else { + final long failureTimeMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - downtimeNanos.get()); + elapsedTimeMillis.add(failureTimeMillis); + } } } finally { @@ -349,15 +411,13 @@ private void doMeasurePerformance( } } - final long min = elapsedTimes.stream().min(Long::compare).orElse(0L); - final long max = elapsedTimes.stream().max(Long::compare).orElse(0L); - final long avg = - (long) elapsedTimes.stream().mapToLong(a -> a).summaryStatistics().getAverage(); + final LongSummaryStatistics stats = elapsedTimeMillis.stream().mapToLong(a -> a).summaryStatistics(); data.paramNetworkOutageDelayMillis = sleepDelayMillis; - data.minFailureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(min); - data.maxFailureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(max); - data.avgFailureDetectionTimeMillis = TimeUnit.NANOSECONDS.toMillis(avg); + data.minFailureDetectionTimeMillis = stats.getMin(); + data.maxFailureDetectionTimeMillis = stats.getMax(); + data.avgFailureDetectionTimeMillis = Math.round(stats.getAverage()); + LOGGER.finest("Collected data: " + data); } private Connection openConnectionWithRetry(Properties props) { @@ -390,8 +450,22 @@ private Connection connectToInstance(Properties props) throws SQLException { return DriverManager.getConnection(url, props); } + private String getQuerySql() { + final DatabaseEngine databaseEngine = + TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine(); + switch (databaseEngine) { + case PG: + return "SELECT pg_sleep(600)"; // 600s -> 10min + case MYSQL: + case MARIADB: + return "SELECT sleep(600)"; // 600s -> 10min + default: + throw new UnsupportedOperationException(databaseEngine.name()); + } + } + private Stream generateFailureDetectionTimeParams() { - // detectionTime, detectionInterval, detectionCount, sleepDelayMS + // detectionTimeMs, detectionIntervalMs, detectionCount, sleepDelayMs return Stream.of( // Defaults Arguments.of(30000, 5000, 3, 5000), @@ -482,6 +556,20 @@ public void writeData(Row row) { cell = row.createCell(6); cell.setCellValue(this.avgFailureDetectionTimeMillis); } + + @Override + public String toString() { + return String.format("%s [\nparamDetectionTime=%d,\nparamDetectionInterval=%d,\nparamDetectionCount=%d,\n" + + "paramNetworkOutageDelayMillis=%d,\nmin=%d,\nmax=%d,\navg=%d ]", + super.toString(), + this.paramDetectionTime, + this.paramDetectionInterval, + this.paramDetectionCount, + this.paramNetworkOutageDelayMillis, + this.minFailureDetectionTimeMillis, + this.maxFailureDetectionTimeMillis, + this.avgFailureDetectionTimeMillis); + } } private static class PerfStatSocketTimeout extends PerfStatBase { @@ -515,5 +603,17 @@ public void writeData(Row row) { cell = row.createCell(4); cell.setCellValue(this.avgFailureDetectionTimeMillis); } + + @Override + public String toString() { + return String.format("%s [\nparamSocketTimeout=%d,\nparamNetworkOutageDelayMillis=%d,\n" + + "min=%d,\nmax=%d,\navg=%d ]", + super.toString(), + this.paramSocketTimeout, + this.paramNetworkOutageDelayMillis, + this.minFailureDetectionTimeMillis, + this.maxFailureDetectionTimeMillis, + this.avgFailureDetectionTimeMillis); + } } } diff --git a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingPerformanceTest.java b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingPerformanceTest.java index 096197b63..4ce2cd424 100644 --- a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingPerformanceTest.java +++ b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingPerformanceTest.java @@ -41,6 +41,7 @@ import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -55,6 +56,7 @@ @ExtendWith(TestDriverProvider.class) @EnableOnTestFeature(TestEnvironmentFeatures.PERFORMANCE) @EnableOnNumOfInstances(min = 5) +@Tag("rw-splitting") public class ReadWriteSplittingPerformanceTest { private static final Logger LOGGER = diff --git a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java index b499b2427..a1c249240 100644 --- a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java +++ b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java @@ -36,8 +36,10 @@ import integration.TestInstanceInfo; import integration.container.ConnectionStringHelper; import integration.container.ProxyHelper; +import integration.container.TestDriver; import integration.container.TestDriverProvider; import integration.container.TestEnvironment; +import integration.container.condition.DisableOnTestDriver; import integration.container.condition.DisableOnTestFeature; import integration.container.condition.EnableOnDatabaseEngine; import integration.container.condition.EnableOnDatabaseEngineDeployment; @@ -106,8 +108,10 @@ protected static Properties getProxiedProps() { protected static Properties getDefaultPropsNoPlugins() { final Properties props = ConnectionStringHelper.getDefaultProperties(); - DriverHelper.setSocketTimeout(props, 10, TimeUnit.SECONDS); - DriverHelper.setConnectTimeout(props, 10, TimeUnit.SECONDS); + props.setProperty( + PropertyDefinition.SOCKET_TIMEOUT.name, String.valueOf(TimeUnit.SECONDS.toMillis(10))); + props.setProperty( + PropertyDefinition.CONNECT_TIMEOUT.name, String.valueOf(TimeUnit.SECONDS.toMillis(10))); return props; } @@ -338,8 +342,13 @@ public void test_setReadOnly_closedConnection() throws SQLException { } } + /** + * PG driver has check of internal readOnly flag and doesn't communicate to a DB server + * if there's no changes. Thus, network exception is not raised. + */ @TestTemplate @EnableOnTestFeature(TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED) + @DisableOnTestDriver(TestDriver.PG) // see comments above public void test_setReadOnlyFalse_allInstancesDown() throws SQLException { try (final Connection conn = DriverManager.getConnection( ConnectionStringHelper.getProxyWrapperUrl(), getProxiedProps())) { @@ -359,6 +368,24 @@ public void test_setReadOnlyFalse_allInstancesDown() throws SQLException { } } + @TestTemplate + @EnableOnTestFeature(TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED) + public void test_setReadOnlyFalse_whenAllInstancesDown() throws SQLException { + try (final Connection conn = DriverManager.getConnection( + ConnectionStringHelper.getWrapperReaderClusterUrl(), getProxiedProps())) { + + // Kill all instances + ProxyHelper.disableAllConnectivity(); + + // setReadOnly(false) triggers switching reader connection to a new writer connection. + // Since connectivity to all instances are down, it's expected to get a network-bound exception + // while opening a new connection to a writer node. + final SQLException exception = + assertThrows(SQLException.class, () -> conn.setReadOnly(false)); + assertEquals(SqlState.CONNECTION_UNABLE_TO_CONNECT.getState(), exception.getSQLState()); + } + } + @TestTemplate public void test_executeWithOldConnection() throws SQLException { try (final Connection conn = DriverManager.getConnection(ConnectionStringHelper.getWrapperUrl(), getProps())) { diff --git a/wrapper/src/test/java/integration/host/TestEnvironment.java b/wrapper/src/test/java/integration/host/TestEnvironment.java index e7113e0c6..21408c884 100644 --- a/wrapper/src/test/java/integration/host/TestEnvironment.java +++ b/wrapper/src/test/java/integration/host/TestEnvironment.java @@ -819,7 +819,8 @@ public void runTests(String taskName) throws IOException, InterruptedException { containerHelper.runCmd(this.testContainer, "./collect_test_results.sh"); assertEquals(0, exitCode, "Hibernate ORM tests failed"); } else { - containerHelper.runTest(this.testContainer, taskName); + TestEnvironmentConfiguration config = new TestEnvironmentConfiguration(); + containerHelper.runTest(this.testContainer, taskName, config.includeTags, config.excludeTags); } } @@ -834,7 +835,8 @@ public void debugTests(String taskName) throws IOException, InterruptedException containerHelper.runCmd(this.testContainer, "./collect_test_results.sh"); assertEquals(0, exitCode, "Hibernate ORM tests failed"); } else { - containerHelper.debugTest(this.testContainer, taskName); + TestEnvironmentConfiguration config = new TestEnvironmentConfiguration(); + containerHelper.debugTest(this.testContainer, taskName, config.includeTags, config.excludeTags); } } diff --git a/wrapper/src/test/java/integration/host/TestEnvironmentConfiguration.java b/wrapper/src/test/java/integration/host/TestEnvironmentConfiguration.java index 238a559ed..9fc5443f3 100644 --- a/wrapper/src/test/java/integration/host/TestEnvironmentConfiguration.java +++ b/wrapper/src/test/java/integration/host/TestEnvironmentConfiguration.java @@ -18,8 +18,10 @@ public class TestEnvironmentConfiguration { - public boolean noDocker = Boolean.parseBoolean(System.getProperty("test-no-docker", "false")); - public boolean noAurora = Boolean.parseBoolean(System.getProperty("test-no-aurora", "false")); + public boolean noDocker = + Boolean.parseBoolean(System.getProperty("test-no-docker", "false")); + public boolean noAurora = + Boolean.parseBoolean(System.getProperty("test-no-aurora", "false")); public boolean noPerformance = Boolean.parseBoolean(System.getProperty("test-no-performance", "false")); public boolean noMysqlEngine = @@ -36,14 +38,39 @@ public class TestEnvironmentConfiguration { Boolean.parseBoolean(System.getProperty("test-no-mariadb-driver", "false")); public boolean noFailover = Boolean.parseBoolean(System.getProperty("test-no-failover", "false")); - public boolean noIam = Boolean.parseBoolean(System.getProperty("test-no-iam", "false")); + public boolean noIam = + Boolean.parseBoolean(System.getProperty("test-no-iam", "false")); public boolean noSecretsManager = Boolean.parseBoolean(System.getProperty("test-no-secrets-manager", "false")); - public boolean noHikari = Boolean.parseBoolean(System.getProperty("test-no-hikari", "false")); - public boolean noGraalVm = Boolean.parseBoolean(System.getProperty("test-no-graalvm", "false")); - public boolean noOpenJdk = Boolean.parseBoolean(System.getProperty("test-no-openjdk", "false")); - public boolean testHibernateOnly = Boolean.parseBoolean(System.getProperty("test-hibernate-only", "false")); - public boolean testAutoscalingOnly = Boolean.parseBoolean(System.getProperty("test-autoscaling-only", "false")); + public boolean noHikari = + Boolean.parseBoolean(System.getProperty("test-no-hikari", "false")); + public boolean noGraalVm = + Boolean.parseBoolean(System.getProperty("test-no-graalvm", "false")); + public boolean noOpenJdk = + Boolean.parseBoolean(System.getProperty("test-no-openjdk", "false")); + public boolean noOpenJdk8 = + Boolean.parseBoolean(System.getProperty("test-no-openjdk8", "false")); + public boolean noOpenJdk11 = + Boolean.parseBoolean(System.getProperty("test-no-openjdk11", "false")); + public boolean testHibernateOnly = + Boolean.parseBoolean(System.getProperty("test-hibernate-only", "false")); + public boolean testAutoscalingOnly = + Boolean.parseBoolean(System.getProperty("test-autoscaling-only", "false")); + + public boolean noInstances1 = + Boolean.parseBoolean(System.getProperty("test-no-instances-1", "false")); + public boolean noInstances2 = + Boolean.parseBoolean(System.getProperty("test-no-instances-2", "false")); + public boolean noInstances5 = + Boolean.parseBoolean(System.getProperty("test-no-instances-5", "false")); + + public boolean noTracesTelemetry = + Boolean.parseBoolean(System.getProperty("test-no-traces-telemetry", "false")); + public boolean noMetricsTelemetry = + Boolean.parseBoolean(System.getProperty("test-no-metrics-telemetry", "false")); + + public String includeTags = System.getProperty("test-include-tags"); + public String excludeTags = System.getProperty("test-exclude-tags"); public String auroraDbRegion = System.getenv("AURORA_DB_REGION"); diff --git a/wrapper/src/test/java/integration/host/TestEnvironmentProvider.java b/wrapper/src/test/java/integration/host/TestEnvironmentProvider.java index 4efa52e50..5aa8da892 100644 --- a/wrapper/src/test/java/integration/host/TestEnvironmentProvider.java +++ b/wrapper/src/test/java/integration/host/TestEnvironmentProvider.java @@ -52,365 +52,106 @@ public Stream provideTestTemplateInvocationContex preCreateInfos.clear(); ArrayList resultContextList = new ArrayList<>(); - final boolean noDocker = Boolean.parseBoolean(System.getProperty("test-no-docker", "false")); - final boolean noAurora = Boolean.parseBoolean(System.getProperty("test-no-aurora", "false")); - final boolean noPerformance = - Boolean.parseBoolean(System.getProperty("test-no-performance", "false")); - final boolean noMysqlEngine = - Boolean.parseBoolean(System.getProperty("test-no-mysql-engine", "false")); - final boolean noMysqlDriver = - Boolean.parseBoolean(System.getProperty("test-no-mysql-driver", "false")); - final boolean noPgEngine = - Boolean.parseBoolean(System.getProperty("test-no-pg-engine", "false")); - final boolean noPgDriver = - Boolean.parseBoolean(System.getProperty("test-no-pg-driver", "false")); - final boolean noMariadbEngine = - Boolean.parseBoolean(System.getProperty("test-no-mariadb-engine", "false")); - final boolean noMariadbDriver = - Boolean.parseBoolean(System.getProperty("test-no-mariadb-driver", "false")); - final boolean noFailover = - Boolean.parseBoolean(System.getProperty("test-no-failover", "false")); - final boolean noIam = Boolean.parseBoolean(System.getProperty("test-no-iam", "false")); - final boolean noSecretsManager = - Boolean.parseBoolean(System.getProperty("test-no-secrets-manager", "false")); - final boolean noHikari = Boolean.parseBoolean(System.getProperty("test-no-hikari", "false")); - final boolean noGraalVm = Boolean.parseBoolean(System.getProperty("test-no-graalvm", "false")); - final boolean noOpenJdk = Boolean.parseBoolean(System.getProperty("test-no-openjdk", "false")); - final boolean testHibernateOnly = Boolean.parseBoolean(System.getProperty("test-hibernate-only", "false")); - final boolean testAutoscalingOnly = Boolean.parseBoolean(System.getProperty("test-autoscaling-only", "false")); - final boolean noTracesTelemetry = Boolean.parseBoolean(System.getProperty("test-no-traces-telemetry", "false")); - final boolean noMetricsTelemetry = Boolean.parseBoolean(System.getProperty("test-no-metrics-telemetry", "false")); - - if (!noDocker) { - if (!noMysqlEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - testHibernateOnly ? TargetJvm.OPENJDK11 : TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testHibernateOnly ? TestEnvironmentFeatures.RUN_HIBERNATE_TESTS_ONLY : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noPgEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - testHibernateOnly ? TargetJvm.OPENJDK11 : TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testHibernateOnly ? TestEnvironmentFeatures.RUN_HIBERNATE_TESTS_ONLY : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMariadbEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MARIADB, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMysqlEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noPgEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMariadbEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MARIADB, - DatabaseInstances.SINGLE_INSTANCE, - 1, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - - // multiple instances - - if (!noMysqlEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - testHibernateOnly ? TargetJvm.OPENJDK11 : TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testHibernateOnly ? TestEnvironmentFeatures.RUN_HIBERNATE_TESTS_ONLY : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noPgEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - testHibernateOnly ? TargetJvm.OPENJDK11 : TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testHibernateOnly ? TestEnvironmentFeatures.RUN_HIBERNATE_TESTS_ONLY : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMariadbEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MARIADB, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMysqlEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noPgEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - if (!noMariadbEngine && !noGraalVm) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MARIADB, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.DOCKER, - TargetJvm.GRAALVM, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED, - // AWS credentials are required for XRay telemetry - noTracesTelemetry && noMetricsTelemetry ? null : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED))); - } - } - - if (!noAurora) { - if (!noMysqlEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.MULTI_INSTANCE, - 5, - DatabaseEngineDeployment.AURORA, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noFailover ? null : TestEnvironmentFeatures.FAILOVER_SUPPORTED, - TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED, - noIam ? null : TestEnvironmentFeatures.IAM, - noSecretsManager ? null : TestEnvironmentFeatures.SECRETS_MANAGER, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noPerformance ? null : TestEnvironmentFeatures.PERFORMANCE, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED))); - - // Tests for HIKARI, IAM, SECRETS_MANAGER and PERFORMANCE are covered by - // cluster configuration above, so it's safe to skip these tests for configurations below. - // The main goal of the following cluster configurations is to check failover. - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.MYSQL, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.AURORA, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noFailover ? null : TestEnvironmentFeatures.FAILOVER_SUPPORTED, - TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED))); - } - if (!noPgEngine && !noOpenJdk) { - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.MULTI_INSTANCE, - 5, - DatabaseEngineDeployment.AURORA, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noFailover ? null : TestEnvironmentFeatures.FAILOVER_SUPPORTED, - TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED, - noIam ? null : TestEnvironmentFeatures.IAM, - noSecretsManager ? null : TestEnvironmentFeatures.SECRETS_MANAGER, - noHikari ? null : TestEnvironmentFeatures.HIKARI, - noPerformance ? null : TestEnvironmentFeatures.PERFORMANCE, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED))); - - // Tests for HIKARI, IAM, SECRETS_MANAGER and PERFORMANCE are covered by - // cluster configuration above, so it's safe to skip these tests for configurations below. - // The main goal of the following cluster configurations is to check failover. - resultContextList.add( - getEnvironment( - new TestEnvironmentRequest( - DatabaseEngine.PG, - DatabaseInstances.MULTI_INSTANCE, - 2, - DatabaseEngineDeployment.AURORA, - TargetJvm.OPENJDK8, - TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, - noFailover ? null : TestEnvironmentFeatures.FAILOVER_SUPPORTED, - TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED, - noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, - noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, - noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, - testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, - noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, - noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED))); + TestEnvironmentConfiguration config = new TestEnvironmentConfiguration(); + + for (DatabaseEngineDeployment deployment : DatabaseEngineDeployment.values()) { + if (deployment == DatabaseEngineDeployment.DOCKER && config.noDocker) { + continue; + } + if (deployment == DatabaseEngineDeployment.AURORA && config.noAurora) { + continue; + } + if (deployment == DatabaseEngineDeployment.RDS) { + // Not in use. + continue; + } + + for (DatabaseEngine engine : DatabaseEngine.values()) { + if (engine == DatabaseEngine.PG && config.noPgEngine) { + continue; + } + if (engine == DatabaseEngine.MYSQL && config.noMysqlEngine) { + continue; + } + if (engine == DatabaseEngine.MARIADB && config.noMariadbEngine) { + continue; + } + + for (DatabaseInstances instances : DatabaseInstances.values()) { + if (deployment == DatabaseEngineDeployment.DOCKER + && instances != DatabaseInstances.SINGLE_INSTANCE) { + continue; + } + + for (int numOfInstances : Arrays.asList(1, 2, 5)) { + if (instances == DatabaseInstances.SINGLE_INSTANCE && numOfInstances > 1) { + continue; + } + if (instances == DatabaseInstances.MULTI_INSTANCE && numOfInstances == 1) { + continue; + } + if (numOfInstances == 1 && config.noInstances1) { + continue; + } + if (numOfInstances == 2 && config.noInstances2) { + continue; + } + if (numOfInstances == 5 && config.noInstances5) { + continue; + } + + for (TargetJvm jvm : TargetJvm.values()) { + if ((jvm == TargetJvm.OPENJDK8 || jvm == TargetJvm.OPENJDK11) && config.noOpenJdk) { + continue; + } + if (jvm == TargetJvm.OPENJDK8 && config.noOpenJdk8) { + continue; + } + if (jvm == TargetJvm.OPENJDK11 && config.noOpenJdk11) { + continue; + } + if (jvm != TargetJvm.OPENJDK11 && config.testHibernateOnly) { + // Run hibernate tests with OPENJDK11 only. + continue; + } + if (jvm == TargetJvm.GRAALVM && config.noGraalVm) { + continue; + } + + + resultContextList.add( + getEnvironment( + new TestEnvironmentRequest( + engine, + instances, + instances == DatabaseInstances.SINGLE_INSTANCE ? 1 : numOfInstances, + deployment, + jvm, + TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED, + deployment == DatabaseEngineDeployment.DOCKER + && config.noTracesTelemetry + && config.noMetricsTelemetry + ? null + : TestEnvironmentFeatures.AWS_CREDENTIALS_ENABLED, + deployment == DatabaseEngineDeployment.DOCKER || config.noFailover + ? null + : TestEnvironmentFeatures.FAILOVER_SUPPORTED, + deployment == DatabaseEngineDeployment.DOCKER || config.noIam + ? null + : TestEnvironmentFeatures.IAM, + config.noSecretsManager ? null : TestEnvironmentFeatures.SECRETS_MANAGER, + config.noHikari ? null : TestEnvironmentFeatures.HIKARI, + config.noPerformance ? null : TestEnvironmentFeatures.PERFORMANCE, + config.noMysqlDriver ? TestEnvironmentFeatures.SKIP_MYSQL_DRIVER_TESTS : null, + config.noPgDriver ? TestEnvironmentFeatures.SKIP_PG_DRIVER_TESTS : null, + config.noMariadbDriver ? TestEnvironmentFeatures.SKIP_MARIADB_DRIVER_TESTS : null, + config.testHibernateOnly ? TestEnvironmentFeatures.RUN_HIBERNATE_TESTS_ONLY : null, + config.testAutoscalingOnly ? TestEnvironmentFeatures.RUN_AUTOSCALING_TESTS_ONLY : null, + config.noTracesTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_TRACES_ENABLED, + config.noMetricsTelemetry ? null : TestEnvironmentFeatures.TELEMETRY_METRICS_ENABLED))); + } + } + } } } diff --git a/wrapper/src/test/java/integration/util/ContainerHelper.java b/wrapper/src/test/java/integration/util/ContainerHelper.java index 32bd30b97..26d63fec7 100644 --- a/wrapper/src/test/java/integration/util/ContainerHelper.java +++ b/wrapper/src/test/java/integration/util/ContainerHelper.java @@ -26,6 +26,7 @@ import eu.rekawek.toxiproxy.ToxiproxyClient; import integration.TestInstanceInfo; import java.io.IOException; +import java.util.ArrayList; import java.util.function.Consumer; import java.util.function.Function; import org.testcontainers.DockerClientFactory; @@ -89,25 +90,59 @@ public Long runCmdInDirectory(GenericContainer container, String workingDirec public void runTest(GenericContainer container, String task) throws IOException, InterruptedException { + runTest(container, task, null, null); + } + + public void runTest(GenericContainer container, String task, String includeTags, String excludeTags) + throws IOException, InterruptedException { System.out.println("==== Container console feed ==== >>>>"); Consumer consumer = new ConsoleConsumer(true); execInContainer(container, consumer, "printenv", "TEST_ENV_DESCRIPTION"); execInContainer(container, consumer, "java", "-version"); - Long exitCode = - execInContainer(container, consumer, "./gradlew", task, "--no-parallel", "--no-daemon"); + + ArrayList commands = new ArrayList<>(); + commands.add("./gradlew"); + commands.add(task); + commands.add("--no-parallel"); + commands.add("--no-daemon"); + if (!StringUtils.isNullOrEmpty(includeTags)) { + commands.add(String.format("-Dtest-include-tags=%s", includeTags.replaceAll(" ", ""))); + } + if (!StringUtils.isNullOrEmpty(excludeTags)) { + commands.add(String.format("-Dtest-exclude-tags=%s", excludeTags.replaceAll(" ", ""))); + } + + Long exitCode = execInContainer(container, consumer, commands.toArray(new String[0])); System.out.println("==== Container console feed ==== <<<<"); assertEquals(0, exitCode, "Some tests failed."); } public void debugTest(GenericContainer container, String task) throws IOException, InterruptedException { + debugTest(container, task, null, null); + } + + public void debugTest(GenericContainer container, String task, String includeTags, String excludeTags) + throws IOException, InterruptedException { System.out.println("==== Container console feed ==== >>>>"); Consumer consumer = new ConsoleConsumer(); execInContainer(container, consumer, "printenv", "TEST_ENV_DESCRIPTION"); execInContainer(container, consumer, "java", "-version"); - Long exitCode = - execInContainer( - container, consumer, "./gradlew", task, "--debug-jvm", "--no-parallel", "--no-daemon"); + + ArrayList commands = new ArrayList<>(); + commands.add("./gradlew"); + commands.add(task); + commands.add("--debug-jvm"); + commands.add("--no-parallel"); + commands.add("--no-daemon"); + if (!StringUtils.isNullOrEmpty(includeTags)) { + commands.add(String.format("-Dtest-include-tags=%s", includeTags.replaceAll(" ", ""))); + } + if (!StringUtils.isNullOrEmpty(excludeTags)) { + commands.add(String.format("-Dtest-exclude-tags=%s", excludeTags.replaceAll(" ", ""))); + } + + Long exitCode = execInContainer(container, consumer, commands.toArray(new String[0])); System.out.println("==== Container console feed ==== <<<<"); assertEquals(0, exitCode, "Some tests failed."); } diff --git a/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginChainBuilderTests.java b/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginChainBuilderTests.java index c837abe49..031cae92d 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginChainBuilderTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginChainBuilderTests.java @@ -71,8 +71,13 @@ public void testSortPlugins() throws SQLException { Properties props = new Properties(); props.put(PropertyDefinition.PLUGINS.name, "iam,efm,failover"); - List result = - builder.getPlugins(mockPluginService, mockConnectionProvider, mockPluginManagerService, props); + List result = builder.getPlugins( + mockPluginService, + mockConnectionProvider, + null, + mockPluginManagerService, + props, + null); assertNotNull(result); assertEquals(4, result.size()); @@ -89,8 +94,13 @@ public void testPreservePluginOrder() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "iam,efm,failover"); props.put(PropertyDefinition.AUTO_SORT_PLUGIN_ORDER.name, "false"); - List result = - builder.getPlugins(mockPluginService, mockConnectionProvider, mockPluginManagerService, props); + List result = builder.getPlugins( + mockPluginService, + mockConnectionProvider, + null, + mockPluginManagerService, + props, + null); assertNotNull(result); assertEquals(4, result.size()); @@ -106,8 +116,13 @@ public void testSortPluginsWithStickToPrior() throws SQLException { Properties props = new Properties(); props.put(PropertyDefinition.PLUGINS.name, "dev,iam,executionTime,connectTime,efm,failover"); - List result = - builder.getPlugins(mockPluginService, mockConnectionProvider, mockPluginManagerService, props); + List result = builder.getPlugins( + mockPluginService, + mockConnectionProvider, + null, + mockPluginManagerService, + props, + null); assertNotNull(result); assertEquals(7, result.size()); diff --git a/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginManagerTests.java b/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginManagerTests.java index ae99e78a0..149e5dde4 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginManagerTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/ConnectionPluginManagerTests.java @@ -49,6 +49,8 @@ import software.amazon.jdbc.plugin.LogQueryConnectionPlugin; import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.ConfigurationProfileBuilder; import software.amazon.jdbc.util.telemetry.TelemetryContext; import software.amazon.jdbc.util.telemetry.TelemetryFactory; import software.amazon.jdbc.wrapper.ConnectionWrapper; @@ -62,6 +64,7 @@ public class ConnectionPluginManagerTests { @Mock TelemetryContext mockTelemetryContext; @Mock PluginService mockPluginService; @Mock PluginManagerService mockPluginManagerService; + ConfigurationProfile configurationProfile = ConfigurationProfileBuilder.get().withName("test").build(); private AutoCloseable closeable; @@ -94,7 +97,7 @@ public void testExecuteJdbcCallA() throws Exception { final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Object result = target.execute( @@ -136,7 +139,7 @@ public void testExecuteJdbcCallB() throws Exception { final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Object result = target.execute( @@ -176,7 +179,7 @@ public void testExecuteJdbcCallC() throws Exception { final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Object result = target.execute( @@ -213,7 +216,7 @@ public void testConnect() throws Exception { final Properties testProperties = new Properties(); final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Connection conn = target.connect("any", new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("anyHost").build(), testProperties, @@ -241,7 +244,7 @@ public void testConnectWithSQLExceptionBefore() { final Properties testProperties = new Properties(); final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); assertThrows( SQLException.class, @@ -267,7 +270,7 @@ public void testConnectWithSQLExceptionAfter() { final Properties testProperties = new Properties(); final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); assertThrows( SQLException.class, @@ -296,7 +299,7 @@ public void testConnectWithUnexpectedExceptionBefore() { final Properties testProperties = new Properties(); final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Exception ex = assertThrows( @@ -324,7 +327,7 @@ public void testConnectWithUnexpectedExceptionAfter() { final Properties testProperties = new Properties(); final ConnectionPluginManager target = new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory); final Exception ex = assertThrows( @@ -357,7 +360,7 @@ public void testExecuteCachedJdbcCallA() throws Exception { final ConnectionPluginManager target = Mockito.spy( new ConnectionPluginManager(mockConnectionProvider, - testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory)); + null, testProperties, testPlugins, mockConnectionWrapper, mockTelemetryFactory)); Object result = target.execute( @@ -435,7 +438,8 @@ public void testExecuteAgainstOldConnection() throws Exception { when(mockOldResultSet.getStatement()).thenReturn(mockOldStatement); final ConnectionPluginManager target = - new ConnectionPluginManager(mockConnectionProvider, testProperties, testPlugins, mockConnectionWrapper, + new ConnectionPluginManager(mockConnectionProvider, + null, testProperties, testPlugins, mockConnectionWrapper, mockPluginService, mockTelemetryFactory); assertThrows(SQLException.class, @@ -465,9 +469,10 @@ public void testDefaultPlugins() throws SQLException { final ConnectionPluginManager target = Mockito.spy(new ConnectionPluginManager( mockConnectionProvider, + null, mockConnectionWrapper, mockTelemetryFactory)); - target.init(mockPluginService, testProperties, mockPluginManagerService); + target.init(mockPluginService, testProperties, mockPluginManagerService, configurationProfile); assertEquals(4, target.plugins.size()); assertEquals(AuroraConnectionTrackerPlugin.class, target.plugins.get(0).getClass()); @@ -483,9 +488,10 @@ public void testNoWrapperPlugins() throws SQLException { final ConnectionPluginManager target = Mockito.spy(new ConnectionPluginManager( mockConnectionProvider, + null, mockConnectionWrapper, mockTelemetryFactory)); - target.init(mockPluginService, testProperties, mockPluginManagerService); + target.init(mockPluginService, testProperties, mockPluginManagerService, configurationProfile); assertEquals(1, target.plugins.size()); } @@ -497,9 +503,10 @@ public void testOverridingDefaultPluginsWithPluginCodes() throws SQLException { final ConnectionPluginManager target = Mockito.spy(new ConnectionPluginManager( mockConnectionProvider, + null, mockConnectionWrapper, mockTelemetryFactory)); - target.init(mockPluginService, testProperties, mockPluginManagerService); + target.init(mockPluginService, testProperties, mockPluginManagerService, configurationProfile); assertEquals(2, target.plugins.size()); assertEquals(LogQueryConnectionPlugin.class, target.plugins.get(0).getClass()); diff --git a/wrapper/src/test/java/software/amazon/jdbc/HikariPooledConnectionProviderTest.java b/wrapper/src/test/java/software/amazon/jdbc/HikariPooledConnectionProviderTest.java index 1f5279520..087c03a79 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/HikariPooledConnectionProviderTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/HikariPooledConnectionProviderTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -47,6 +48,8 @@ import software.amazon.jdbc.HikariPooledConnectionProvider.PoolKey; import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.targetdriverdialect.ConnectInfo; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.SlidingExpirationCache; class HikariPooledConnectionProviderTest { @@ -55,6 +58,7 @@ class HikariPooledConnectionProviderTest { @Mock HostSpec mockHostSpec; @Mock HikariConfig mockConfig; @Mock Dialect mockDialect; + @Mock TargetDriverDialect mockTargetDriverDialect; @Mock HikariDataSource dsWithNoConnections; @Mock HikariDataSource dsWith1Connection; @Mock HikariDataSource dsWith2Connections; @@ -73,9 +77,9 @@ class HikariPooledConnectionProviderTest { private final String readerUrl1Connection = "readerWith1connection.XYZ.us-east-1.rds.amazonaws.com"; private final HostSpec readerHost1Connection = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host(readerUrl1Connection).port(port).role(HostRole.READER).build(); - private final String readerUrl2Connections = "readerWith2connections.XYZ.us-east-1.rds.amazonaws.com"; - private final HostSpec readerHost2Connections = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host(readerUrl2Connections).port(port).role(HostRole.READER).build(); + private final String readerUrl2Connection = "readerWith2connection.XYZ.us-east-1.rds.amazonaws.com"; + private final HostSpec readerHost2Connection = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) + .host(readerUrl2Connection).port(port).role(HostRole.READER).build(); private final String protocol = "protocol://"; private final Properties defaultProps = getDefaultProps(); @@ -88,7 +92,7 @@ private List getTestHosts() { List hosts = new ArrayList<>(); hosts.add(writerHostNoConnections); hosts.add(readerHost1Connection); - hosts.add(readerHost2Connections); + hosts.add(readerHost2Connection); return hosts; } @@ -130,12 +134,14 @@ void testConnectWithDefaultMapping() throws SQLException { provider = spy(new HikariPooledConnectionProvider((hostSpec, properties) -> mockConfig)); - doReturn(mockDataSource).when(provider).createHikariDataSource(any(), any(), any()); + doReturn(mockDataSource).when(provider).createHikariDataSource(any(), any(), any(), any()); + doReturn(new ConnectInfo("url", new Properties())) + .when(mockTargetDriverDialect).prepareConnectInfo(anyString(), any(), any()); Properties props = new Properties(); props.setProperty(PropertyDefinition.USER.name, user1); props.setProperty(PropertyDefinition.PASSWORD.name, password); - try (Connection conn = provider.connect(protocol, mockDialect, mockHostSpec, props)) { + try (Connection conn = provider.connect(protocol, mockDialect, mockTargetDriverDialect, mockHostSpec, props)) { assertEquals(mockConnection, conn); assertEquals(1, provider.getHostCount()); final Set hosts = provider.getHosts(); @@ -155,12 +161,12 @@ void testConnectWithCustomMapping() throws SQLException { (hostSpec, properties) -> mockConfig, (hostSpec, properties) -> hostSpec.getUrl() + "+someUniqueKey")); - doReturn(mockDataSource).when(provider).createHikariDataSource(any(), any(), any()); + doReturn(mockDataSource).when(provider).createHikariDataSource(any(), any(), any(), any()); Properties props = new Properties(); props.setProperty(PropertyDefinition.USER.name, user1); props.setProperty(PropertyDefinition.PASSWORD.name, password); - try (Connection conn = provider.connect(protocol, mockDialect, mockHostSpec, props)) { + try (Connection conn = provider.connect(protocol, mockDialect, mockTargetDriverDialect, mockHostSpec, props)) { assertEquals(mockConnection, conn); assertEquals(1, provider.getHostCount()); final Set keys = provider.getKeys(); @@ -175,13 +181,23 @@ public void testAcceptsUrl() { assertTrue( provider.acceptsUrl(protocol, - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host(readerUrl2Connections).build(), + new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host(readerUrl2Connection).build(), defaultProps)); assertFalse( provider.acceptsUrl(protocol, new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host(clusterUrl).build(), defaultProps)); } + @Test + public void testRandomStrategy() throws SQLException { + provider = new HikariPooledConnectionProvider((hostSpec, properties) -> mockConfig); + provider.setDatabasePools(getTestPoolMap()); + + HostSpec selectedHost = provider.getHostSpecByStrategy(testHosts, HostRole.READER, "random", defaultProps); + assertTrue(readerUrl1Connection.equals(selectedHost.getHost()) + || readerUrl2Connection.equals(selectedHost.getHost())); + } + @Test public void testLeastConnectionsStrategy() throws SQLException { provider = new HikariPooledConnectionProvider((hostSpec, properties) -> mockConfig); @@ -194,9 +210,9 @@ public void testLeastConnectionsStrategy() throws SQLException { private SlidingExpirationCache getTestPoolMap() { SlidingExpirationCache map = new SlidingExpirationCache<>(); - map.computeIfAbsent(new PoolKey(readerHost2Connections.getUrl(), user1), + map.computeIfAbsent(new PoolKey(readerHost2Connection.getUrl(), user1), (key) -> dsWith1Connection, TimeUnit.MINUTES.toNanos(10)); - map.computeIfAbsent(new PoolKey(readerHost2Connections.getUrl(), user2), + map.computeIfAbsent(new PoolKey(readerHost2Connection.getUrl(), user2), (key) -> dsWith1Connection, TimeUnit.MINUTES.toNanos(10)); map.computeIfAbsent(new PoolKey(readerHost1Connection.getUrl(), user1), (key) -> dsWith1Connection, TimeUnit.MINUTES.toNanos(10)); @@ -204,12 +220,14 @@ private SlidingExpirationCache getTestPoolMap() { } @Test - public void testConfigurePool() { + public void testConfigurePool() throws SQLException { provider = new HikariPooledConnectionProvider((hostSpec, properties) -> mockConfig); final String expectedJdbcUrl = protocol + readerHost1Connection.getUrl() + db + "?database=" + db; + doReturn(new ConnectInfo(protocol + readerHost1Connection.getUrl() + db, defaultProps)) + .when(mockTargetDriverDialect).prepareConnectInfo(anyString(), any(), any()); - provider.configurePool(mockConfig, protocol, readerHost1Connection, defaultProps); + provider.configurePool(mockConfig, protocol, readerHost1Connection, defaultProps, mockTargetDriverDialect); verify(mockConfig).setJdbcUrl(expectedJdbcUrl); verify(mockConfig).setUsername(user1); verify(mockConfig).setPassword(password); @@ -220,10 +238,10 @@ public void testConnectToDeletedInstance() throws SQLException { provider = spy(new HikariPooledConnectionProvider((hostSpec, properties) -> mockConfig)); doReturn(mockDataSource).when(provider) - .createHikariDataSource(eq(protocol), eq(readerHost1Connection), eq(defaultProps)); + .createHikariDataSource(eq(protocol), eq(readerHost1Connection), eq(defaultProps), eq(mockTargetDriverDialect)); when(mockDataSource.getConnection()).thenThrow(SQLException.class); assertThrows(SQLException.class, - () -> provider.connect(protocol, mockDialect, readerHost1Connection, defaultProps)); + () -> provider.connect(protocol, mockDialect, mockTargetDriverDialect, readerHost1Connection, defaultProps)); } } diff --git a/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java b/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java index dce989fce..2fca72b60 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java @@ -62,6 +62,10 @@ import software.amazon.jdbc.exceptions.ExceptionManager; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.ConfigurationProfileBuilder; +import software.amazon.jdbc.states.SessionStateService; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; public class PluginServiceImplTests { @@ -75,8 +79,11 @@ public class PluginServiceImplTests { @Mock Connection oldConnection; @Mock HostListProvider hostListProvider; @Mock DialectManager dialectManager; + @Mock TargetDriverDialect mockTargetDriverDialect; @Mock Statement statement; @Mock ResultSet resultSet; + ConfigurationProfile configurationProfile = ConfigurationProfileBuilder.get().withName("test").build(); + @Mock SessionStateService sessionStateService; @Captor ArgumentCaptor> argumentChanges; @Captor ArgumentCaptor>> argumentChangesMap; @@ -104,13 +111,19 @@ public void testOldConnectionNoSuggestion() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("old-host") .build(); - - target.setCurrentConnection(newConnection, new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("new-host").build()); @@ -127,7 +140,15 @@ public void testOldConnectionDisposeSuggestion() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("old-host") .build(); @@ -148,7 +169,15 @@ public void testOldConnectionPreserveSuggestion() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("old-host") .build(); @@ -173,7 +202,15 @@ public void testOldConnectionMixedSuggestion() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("old-host") .build(); @@ -195,7 +232,15 @@ public void testChangesNewConnectionNewHostNewPortNewRoleNewAvailability() throw PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("old-host").port(1000).role(HostRole.WRITER).availability(HostAvailability.AVAILABLE).build(); @@ -226,7 +271,15 @@ public void testChangesNewConnectionNewRoleNewAvailability() throws SQLException PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) @@ -257,7 +310,15 @@ public void testChangesNewConnection() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) @@ -288,7 +349,15 @@ public void testChangesNoChanges() throws SQLException { PluginServiceImpl target = spy(new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.currentConnection = oldConnection; target.currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("old-host").port(1000).role(HostRole.READER).availability(HostAvailability.AVAILABLE).build(); @@ -311,7 +380,15 @@ public void testSetNodeListAdded() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = new ArrayList<>(); target.hostListProvider = hostListProvider; @@ -337,7 +414,15 @@ public void testSetNodeListDeleted() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Arrays.asList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("hostA").build(), new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("hostB").build()); @@ -366,7 +451,15 @@ public void testSetNodeListChanged() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Collections.singletonList(new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("hostA").port(HostSpec.NO_PORT).role(HostRole.WRITER).build()); target.hostListProvider = hostListProvider; @@ -395,7 +488,15 @@ public void testSetNodeListNoChanges() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Collections.singletonList(new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("hostA").port(HostSpec.NO_PORT).role(HostRole.READER).build()); target.hostListProvider = hostListProvider; @@ -413,7 +514,15 @@ public void testNodeAvailabilityNotChanged() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Collections.singletonList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("hostA").port(HostSpec.NO_PORT).role(HostRole.READER).availability(HostAvailability.AVAILABLE) @@ -434,7 +543,15 @@ public void testNodeAvailabilityChanged_WentDown() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Collections.singletonList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("hostA").port(HostSpec.NO_PORT).role(HostRole.READER).availability(HostAvailability.AVAILABLE) @@ -462,7 +579,15 @@ public void testNodeAvailabilityChanged_WentUp() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Collections.singletonList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) .host("hostA").port(HostSpec.NO_PORT).role(HostRole.READER).availability(HostAvailability.NOT_AVAILABLE) @@ -501,7 +626,15 @@ public void testNodeAvailabilityChanged_WentUp_ByAlias() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Arrays.asList(hostA, hostB); @@ -538,7 +671,15 @@ public void testNodeAvailabilityChanged_WentUp_MultipleHostsByAlias() throws SQL PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hosts = Arrays.asList(hostA, hostB); @@ -608,7 +749,15 @@ void testRefreshHostList_withCachedHostAvailability() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); when(target.getHostListProvider()).thenReturn(hostListProvider); assertNotEquals(expectedHostSpecs, newHostSpecs); @@ -657,7 +806,15 @@ void testForceRefreshHostList_withCachedHostAvailability() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); when(target.getHostListProvider()).thenReturn(hostListProvider); assertNotEquals(expectedHostSpecs, newHostSpecs); @@ -674,7 +831,15 @@ void testForceRefreshHostList_withCachedHostAvailability() throws SQLException { void testIdentifyConnectionWithNoAliases() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); when(target.getHostListProvider()).thenReturn(hostListProvider); when(target.getDialect()).thenReturn(new MysqlDialect()); @@ -687,7 +852,15 @@ void testIdentifyConnectionWithAliases() throws SQLException { .build(); PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hostListProvider = hostListProvider; when(target.getHostListProvider()).thenReturn(hostListProvider); when(hostListProvider.identifyConnection(eq(newConnection))).thenReturn(expected); @@ -707,7 +880,15 @@ void testFillAliasesNonEmptyAliases() throws SQLException { PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); assertEquals(1, oneAlias.getAliases().size()); target.fillAliases(newConnection, oneAlias); @@ -721,7 +902,15 @@ void testFillAliasesWithInstanceEndpoint(Dialect dialect, String[] expectedInsta final HostSpec empty = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("foo").build(); PluginServiceImpl target = spy( new PluginServiceImpl( - pluginManager, new ExceptionManager(), PROPERTIES, URL, DRIVER_PROTOCOL, dialectManager)); + pluginManager, + new ExceptionManager(), + PROPERTIES, + URL, + DRIVER_PROTOCOL, + dialectManager, + mockTargetDriverDialect, + configurationProfile, + sessionStateService)); target.hostListProvider = hostListProvider; when(target.getDialect()).thenReturn(dialect); when(resultSet.next()).thenReturn(true, false); // Result set contains 1 row. diff --git a/wrapper/src/test/java/software/amazon/jdbc/ds/AwsWrapperDataSourceTest.java b/wrapper/src/test/java/software/amazon/jdbc/ds/AwsWrapperDataSourceTest.java index dfef208d1..f1ce0b84b 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/ds/AwsWrapperDataSourceTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/ds/AwsWrapperDataSourceTest.java @@ -54,7 +54,8 @@ void setUp() throws SQLException { ds.setTargetDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); doReturn(mockConnection) .when(ds) - .createConnectionWrapper(propertiesArgumentCaptor.capture(), urlArgumentCaptor.capture(), any(), any()); + .createConnectionWrapper( + propertiesArgumentCaptor.capture(), urlArgumentCaptor.capture(), any(), any(), any(), any(), any()); } @AfterEach diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPluginTest.java index 144e80d35..426ab6b0d 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/AuroraConnectionTrackerPluginTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.JdbcCallable; @@ -120,8 +121,16 @@ public void testTrackNewInstanceConnections( @Test public void testInvalidateOpenedConnectionsWhenWriterHostNotChange() throws SQLException { final FailoverSQLException expectedException = new FailoverSQLException("reason", "sqlstate"); - final HostSpec originalHost = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("host") + final HostSpec originalHost = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) + .host("host") + .role(HostRole.WRITER) + .build(); + final HostSpec newHost = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) + .host("new-host") + .role(HostRole.WRITER) .build(); + + // Host list changes during simulated failover when(mockPluginService.getHosts()).thenReturn(Collections.singletonList(originalHost)); doThrow(expectedException).when(mockSqlFunction).call(); diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPluginTest.java index d27eb3cfe..041778e61 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPluginTest.java @@ -66,6 +66,10 @@ import software.amazon.jdbc.exceptions.MySQLExceptionHandler; import software.amazon.jdbc.exceptions.PgExceptionHandler; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.profile.ConfigurationProfile; +import software.amazon.jdbc.profile.ConfigurationProfileBuilder; +import software.amazon.jdbc.states.SessionStateService; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.telemetry.GaugeCallable; import software.amazon.jdbc.util.telemetry.TelemetryContext; @@ -113,6 +117,10 @@ public class AwsSecretsManagerConnectionPluginTest { @Mock TelemetryContext mockTelemetryContext; @Mock TelemetryCounter mockTelemetryCounter; @Mock TelemetryGauge mockTelemetryGauge; + @Mock TargetDriverDialect mockTargetDriverDialect; + ConfigurationProfile configurationProfile = ConfigurationProfileBuilder.get().withName("test").build(); + + @Mock SessionStateService mockSessionStateService; @BeforeEach public void init() throws SQLException { @@ -239,7 +247,10 @@ public void testConnectWithNewSecretsAfterTryingWithCachedSecrets( TEST_PROPS, "url", protocol, - mockDialectManager), + mockDialectManager, + mockTargetDriverDialect, + configurationProfile, + mockSessionStateService), TEST_PROPS, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest); @@ -336,7 +347,10 @@ public void testFailedInitialConnectionWithWrappedGenericError(final String acce TEST_PROPS, "url", TEST_PG_PROTOCOL, - mockDialectManager), + mockDialectManager, + mockTargetDriverDialect, + configurationProfile, + mockSessionStateService), TEST_PROPS, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest); @@ -375,7 +389,10 @@ public void testConnectWithWrappedMySQLException() throws SQLException { TEST_PROPS, "url", TEST_MYSQL_PROTOCOL, - mockDialectManager), + mockDialectManager, + mockTargetDriverDialect, + configurationProfile, + mockSessionStateService), TEST_PROPS, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest); @@ -413,7 +430,10 @@ public void testConnectWithWrappedPostgreSQLException() throws SQLException { TEST_PROPS, "url", TEST_PG_PROTOCOL, - mockDialectManager), + mockDialectManager, + mockTargetDriverDialect, + configurationProfile, + mockSessionStateService), TEST_PROPS, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest); @@ -451,7 +471,7 @@ public void testConnectViaARN(final String arn, final Region expectedRegionParse SECRET_ID_PROPERTY.set(props, arn); this.plugin = spy(new AwsSecretsManagerConnectionPlugin( - new PluginServiceImpl(mockConnectionPluginManager, props, "url", TEST_PG_PROTOCOL), + new PluginServiceImpl(mockConnectionPluginManager, props, "url", TEST_PG_PROTOCOL, mockTargetDriverDialect), props, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest)); @@ -471,7 +491,7 @@ public void testConnectionWithRegionParameterAndARN(final String arn, final Regi REGION_PROPERTY.set(props, expectedRegion.toString()); this.plugin = spy(new AwsSecretsManagerConnectionPlugin( - new PluginServiceImpl(mockConnectionPluginManager, props, "url", TEST_PG_PROTOCOL), + new PluginServiceImpl(mockConnectionPluginManager, props, "url", TEST_PG_PROTOCOL, mockTargetDriverDialect), props, (host, r) -> mockSecretsManagerClient, (id) -> mockGetValueRequest)); diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/DefaultConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/DefaultConnectionPluginTest.java index 45c84d8ac..d8fac47be 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/DefaultConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/DefaultConnectionPluginTest.java @@ -71,7 +71,6 @@ class DefaultConnectionPluginTest { @Mock TelemetryCounter mockTelemetryCounter; @Mock TelemetryGauge mockTelemetryGauge; @Mock ConnectionProviderManager mockConnectionProviderManager; - @Mock ConnectionProvider mockConnectionProvider; @Mock HostSpec mockHostSpec; @@ -88,10 +87,10 @@ void setUp() { // noinspection unchecked when(mockTelemetryFactory.createGauge(anyString(), any(GaugeCallable.class))).thenReturn(mockTelemetryGauge); when(mockConnectionProviderManager.getConnectionProvider(anyString(), any(), any())) - .thenReturn(mockConnectionProvider); + .thenReturn(connectionProvider); plugin = new DefaultConnectionPlugin( - pluginService, connectionProvider, pluginManagerService, mockConnectionProviderManager); + pluginService, connectionProvider, null, pluginManagerService, mockConnectionProviderManager); } @AfterEach @@ -123,7 +122,7 @@ void testExecute_closeOldConnection() throws SQLException { @Test void testConnect() throws SQLException { plugin.connect("anyProtocol", mockHostSpec, new Properties(), true, mockConnectFunction); - verify(mockConnectionProvider, atLeastOnce()).connect(anyString(), any(), any(), any()); + verify(connectionProvider, atLeastOnce()).connect(anyString(), any(), any(), any(), any()); verify(mockConnectionProviderManager, atLeastOnce()).initConnection(any(), anyString(), any(), any()); } diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/ExecutionTimeConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/ExecutionTimeConnectionPluginTest.java index af39a8f81..54aef99a5 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/ExecutionTimeConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/ExecutionTimeConnectionPluginTest.java @@ -63,6 +63,10 @@ void test_executeTime() throws SQLException, UnsupportedEncodingException { final StreamHandler handler = new StreamHandler(os, new SimpleFormatter()); handler.setLevel(Level.ALL); logger.addHandler(handler); + logger.setLevel(Level.ALL); + + final Logger packageLogger = Logger.getLogger("software.amazon.jdbc"); + packageLogger.setLevel(Level.ALL); final ExecutionTimeConnectionPlugin plugin = new ExecutionTimeConnectionPlugin(); @@ -79,4 +83,4 @@ void test_executeTime() throws SQLException, UnsupportedEncodingException { assertTrue(logMessages.contains("Executed Statement.executeQuery in")); } -} \ No newline at end of file +} diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/IamAuthConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/IamAuthConnectionPluginTest.java index 2323859ec..63741b7b5 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/IamAuthConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/IamAuthConnectionPluginTest.java @@ -102,7 +102,7 @@ public static void registerDrivers() throws SQLException { @Test public void testPostgresConnectValidTokenInCache() throws SQLException { IamAuthConnectionPlugin.tokenCache.put(PG_CACHE_KEY, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); when(mockDialect.getDefaultPort()).thenReturn(DEFAULT_PG_PORT); @@ -114,7 +114,7 @@ public void testMySqlConnectValidTokenInCache() throws SQLException { props.setProperty(PropertyDefinition.USER.name, "mysqlUser"); props.setProperty(PropertyDefinition.PASSWORD.name, "mysqlPassword"); IamAuthConnectionPlugin.tokenCache.put(MYSQL_CACHE_KEY, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); when(mockDialect.getDefaultPort()).thenReturn(DEFAULT_MYSQL_PORT); @@ -130,7 +130,7 @@ public void testPostgresConnectWithInvalidPortFallbacksToHostPort() throws SQLEx final String cacheKeyWithNewPort = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + PG_HOST_SPEC_WITH_PORT.getPort() + ":postgresqlUser"; IamAuthConnectionPlugin.tokenCache.put(cacheKeyWithNewPort, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); testTokenSetInProps(PG_DRIVER_PROTOCOL, PG_HOST_SPEC_WITH_PORT); } @@ -145,7 +145,7 @@ public void testPostgresConnectWithInvalidPortAndNoHostPortFallbacksToHostPort() final String cacheKeyWithNewPort = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + DEFAULT_PG_PORT + ":postgresqlUser"; IamAuthConnectionPlugin.tokenCache.put(cacheKeyWithNewPort, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); testTokenSetInProps(PG_DRIVER_PROTOCOL, PG_HOST_SPEC); } @@ -153,7 +153,7 @@ public void testPostgresConnectWithInvalidPortAndNoHostPortFallbacksToHostPort() @Test public void testConnectExpiredTokenInCache() throws SQLException { IamAuthConnectionPlugin.tokenCache.put(PG_CACHE_KEY, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().minusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().minusMillis(300000))); when(mockDialect.getDefaultPort()).thenReturn(DEFAULT_PG_PORT); @@ -171,7 +171,7 @@ public void testConnectEmptyCache() throws SQLException { public void testConnectWithSpecifiedPort() throws SQLException { final String cacheKeyWithNewPort = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:1234:" + "postgresqlUser"; IamAuthConnectionPlugin.tokenCache.put(cacheKeyWithNewPort, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); testTokenSetInProps(PG_DRIVER_PROTOCOL, PG_HOST_SPEC_WITH_PORT); } @@ -183,7 +183,7 @@ public void testConnectWithSpecifiedIamDefaultPort() throws SQLException { final String cacheKeyWithNewPort = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + iamDefaultPort + ":postgresqlUser"; IamAuthConnectionPlugin.tokenCache.put(cacheKeyWithNewPort, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); testTokenSetInProps(PG_DRIVER_PROTOCOL, PG_HOST_SPEC_WITH_PORT); } @@ -194,7 +194,7 @@ public void testConnectWithSpecifiedRegion() throws SQLException { "us-west-1:pg.testdb.us-west-1.rds.amazonaws.com:" + DEFAULT_PG_PORT + ":" + "postgresqlUser"; props.setProperty(IamAuthConnectionPlugin.IAM_REGION.name, "us-west-1"); IamAuthConnectionPlugin.tokenCache.put(cacheKeyWithNewRegion, - new IamAuthConnectionPlugin.TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); + new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000))); when(mockDialect.getDefaultPort()).thenReturn(DEFAULT_PG_PORT); diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/dev/DeveloperConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/dev/DeveloperConnectionPluginTest.java index 0c1e27d6b..a7bdfb54a 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/dev/DeveloperConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/dev/DeveloperConnectionPluginTest.java @@ -42,6 +42,7 @@ import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.dialect.DialectCodes; import software.amazon.jdbc.dialect.DialectManager; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.telemetry.TelemetryContext; import software.amazon.jdbc.util.telemetry.TelemetryFactory; import software.amazon.jdbc.wrapper.ConnectionWrapper; @@ -55,6 +56,7 @@ public class DeveloperConnectionPluginTest { @Mock ExceptionSimulatorConnectCallback mockConnectCallback; @Mock private TelemetryFactory mockTelemetryFactory; @Mock TelemetryContext mockTelemetryContext; + @Mock TargetDriverDialect mockTargetDriverDialect; private AutoCloseable closeable; @@ -67,7 +69,7 @@ void cleanUp() throws Exception { void init() throws SQLException { closeable = MockitoAnnotations.openMocks(this); - when(mockConnectionProvider.connect(any(), any(), any(), any())).thenReturn(mockConnection); + when(mockConnectionProvider.connect(any(), any(), any(), any(), any())).thenReturn(mockConnection); when(mockConnectCallback.getExceptionToRaise(any(), any(), any(), anyBoolean())).thenReturn(null); when(mockService.getTelemetryFactory()).thenReturn(mockTelemetryFactory); @@ -83,7 +85,13 @@ public void test_RaiseException() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -106,7 +114,13 @@ public void test_RaiseExceptionForMethodName() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -129,7 +143,13 @@ public void test_RaiseExceptionForAnyMethodName() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -152,7 +172,13 @@ public void test_RaiseExceptionForWrongMethodName() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -177,7 +203,13 @@ public void test_RaiseExpectedExceptionClass() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -200,7 +232,13 @@ public void test_RaiseUnexpectedExceptionClass() throws SQLException { props.put(PropertyDefinition.PLUGINS.name, "dev"); props.put(DialectManager.DIALECT.name, DialectCodes.PG); try (ConnectionWrapper wrapper = new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)) { + props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)) { ExceptionSimulator simulator = wrapper.unwrap(ExceptionSimulator.class); assertNotNull(simulator); @@ -232,13 +270,23 @@ public void test_RaiseExceptionOnConnect() { Throwable thrownException = assertThrows( SQLException.class, - () -> new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)); + () -> new ConnectionWrapper(props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)); assertSame(exception, thrownException); assertDoesNotThrow( - () -> new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)); + () -> new ConnectionWrapper(props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)); } @Test @@ -251,8 +299,13 @@ public void test_NoExceptionOnConnectWithCallback() { ExceptionSimulatorManager.setCallback(mockConnectCallback); assertDoesNotThrow( - () -> new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)); + () -> new ConnectionWrapper(props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)); } @Test @@ -270,12 +323,22 @@ public void test_RaiseExceptionOnConnectWithCallback() { Throwable thrownException = assertThrows( SQLException.class, - () -> new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)); + () -> new ConnectionWrapper(props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)); assertSame(exception, thrownException); assertDoesNotThrow( - () -> new ConnectionWrapper( - props, "any-protocol://any-host/", mockConnectionProvider, mockTelemetryFactory)); + () -> new ConnectionWrapper(props, + "any-protocol://any-host/", + mockConnectionProvider, + null, + mockTargetDriverDialect, + null, + mockTelemetryFactory)); } } diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/efm/ConcurrencyTests.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/efm/ConcurrencyTests.java index 62007ac73..00948a72c 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/efm/ConcurrencyTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/efm/ConcurrencyTests.java @@ -41,6 +41,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.concurrent.Executor; @@ -69,7 +70,9 @@ import software.amazon.jdbc.dialect.UnknownDialect; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; -import software.amazon.jdbc.states.SessionDirtyFlag; +import software.amazon.jdbc.states.SessionStateService; +import software.amazon.jdbc.targetdriverdialect.PgTargetDriverDialect; +import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.telemetry.TelemetryFactory; @Disabled @@ -256,67 +259,185 @@ public synchronized String format(LogRecord lr) { executor.shutdownNow(); } - public static class TestPluginService implements PluginService { + public static class TestSessionStateService implements SessionStateService { - private final HostSpec hostSpec; - private final Connection connection; + @Override + public Optional getAutoCommit() throws SQLException { + return Optional.empty(); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { - public TestPluginService(HostSpec hostSpec, Connection connection) { - this.hostSpec = hostSpec; - this.connection = connection; } @Override - public Connection getCurrentConnection() { - return this.connection; + public void setupPristineAutoCommit() throws SQLException { + } @Override - public HostSpec getCurrentHostSpec() { - return this.hostSpec; + public Optional getReadOnly() throws SQLException { + return Optional.empty(); } @Override - public void setCurrentConnection(@NonNull Connection connection, @NonNull HostSpec hostSpec) - throws SQLException { + public void setReadOnly(boolean readOnly) throws SQLException { } @Override - public EnumSet setCurrentConnection(@NonNull Connection connection, - @NonNull HostSpec hostSpec, @Nullable ConnectionPlugin skipNotificationForThisPlugin) - throws SQLException { - return null; + public void setupPristineReadOnly() throws SQLException { + + } + + @Override + public Optional getCatalog() throws SQLException { + return Optional.empty(); } @Override - public EnumSet getCurrentConnectionState() { - return EnumSet.noneOf(SessionDirtyFlag.class); + public void setCatalog(String catalog) throws SQLException { + } @Override - public void setCurrentConnectionState(SessionDirtyFlag flag) { + public void setupPristineCatalog() throws SQLException { } @Override - public void resetCurrentConnectionState(SessionDirtyFlag flag) { + public Optional getHoldability() throws SQLException { + return Optional.empty(); + } + + @Override + public void setHoldability(int holdability) throws SQLException { } @Override - public void resetCurrentConnectionStates() { + public void setupPristineHoldability() throws SQLException { } @Override - public boolean getAutoCommit() { - return false; + public Optional getNetworkTimeout() throws SQLException { + return Optional.empty(); + } + + @Override + public void setNetworkTimeout(int milliseconds) throws SQLException { + + } + + @Override + public void setupPristineNetworkTimeout() throws SQLException { + + } + + @Override + public Optional getSchema() throws SQLException { + return Optional.empty(); + } + + @Override + public void setSchema(String schema) throws SQLException { + + } + + @Override + public void setupPristineSchema() throws SQLException { + + } + + @Override + public Optional getTransactionIsolation() throws SQLException { + return Optional.empty(); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + + } + + @Override + public void setupPristineTransactionIsolation() throws SQLException { + + } + + @Override + public Optional>> getTypeMap() throws SQLException { + return Optional.empty(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + + } + + @Override + public void setupPristineTypeMap() throws SQLException { + + } + + @Override + public void reset() { + + } + + @Override + public void begin() throws SQLException { + + } + + @Override + public void complete() { + + } + + @Override + public void applyCurrentSessionState(Connection newConnection) throws SQLException { + } @Override - public void setAutoCommit(boolean autoCommit) { + public void applyPristineSessionState(Connection connection) throws SQLException { + + } + } + + public static class TestPluginService implements PluginService { + private final HostSpec hostSpec; + private final Connection connection; + + public TestPluginService(HostSpec hostSpec, Connection connection) { + this.hostSpec = hostSpec; + this.connection = connection; + } + + @Override + public Connection getCurrentConnection() { + return this.connection; + } + + @Override + public HostSpec getCurrentHostSpec() { + return this.hostSpec; + } + + @Override + public void setCurrentConnection(@NonNull Connection connection, @NonNull HostSpec hostSpec) + throws SQLException { + + } + + @Override + public EnumSet setCurrentConnection(@NonNull Connection connection, + @NonNull HostSpec hostSpec, @Nullable ConnectionPlugin skipNotificationForThisPlugin) + throws SQLException { + return null; } @Override @@ -348,16 +469,6 @@ public HostRole getHostRole(Connection conn) { public void setAvailability(Set hostAliases, HostAvailability availability) { } - @Override - public boolean isExplicitReadOnly() { - return false; - } - - @Override - public boolean isReadOnly() { - return false; - } - @Override public boolean isInTransaction() { return false; @@ -404,6 +515,11 @@ public String getTargetName() { return null; } + @Override + public @NonNull SessionStateService getSessionStateService() { + return new TestSessionStateService(); + } + @Override public boolean isNetworkException(Throwable throwable) { return false; @@ -429,6 +545,11 @@ public Dialect getDialect() { return new UnknownDialect(); } + @Override + public TargetDriverDialect getTargetDriverDialect() { + return new PgTargetDriverDialect(); + } + public void updateDialect(final @NonNull Connection connection) throws SQLException { } @Override diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandlerTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandlerTest.java index 49a4bed37..3157c91d4 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandlerTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandlerTest.java @@ -111,7 +111,6 @@ public void testReconnectToWriter_taskBReaderException() throws SQLException { assertSame(result.getNewConnection(), mockConnection); final InOrder inOrder = Mockito.inOrder(mockPluginService); - inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.AVAILABLE)); } @@ -153,7 +152,6 @@ public void testReconnectToWriter_SlowReaderA() throws SQLException { assertSame(result.getNewConnection(), mockWriterConnection); final InOrder inOrder = Mockito.inOrder(mockPluginService); - inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.AVAILABLE)); } @@ -196,7 +194,6 @@ public void testReconnectToWriter_taskBDefers() throws SQLException { assertSame(result.getNewConnection(), mockWriterConnection); final InOrder inOrder = Mockito.inOrder(mockPluginService); - inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); inOrder.verify(mockPluginService).setAvailability(eq(writer.asAliases()), eq(HostAvailability.AVAILABLE)); } @@ -243,7 +240,6 @@ public void testConnectToReaderA_SlowWriter() throws SQLException { assertEquals(3, result.getTopology().size()); assertEquals("new-writer-host", result.getTopology().get(0).getHost()); - verify(mockPluginService, times(1)).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); verify(mockPluginService, times(1)).setAvailability(eq(newWriterHost.asAliases()), eq(HostAvailability.AVAILABLE)); } @@ -291,7 +287,6 @@ public void testConnectToReaderA_taskADefers() throws SQLException { assertEquals("new-writer-host", result.getTopology().get(0).getHost()); verify(mockPluginService, atLeastOnce()).forceRefreshHostList(any(Connection.class)); - verify(mockPluginService, times(1)).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); verify(mockPluginService, times(1)).setAvailability(eq(newWriterHost.asAliases()), eq(HostAvailability.AVAILABLE)); } @@ -343,7 +338,6 @@ public void testFailedToConnect_failoverTimeout() throws SQLException { assertFalse(result.isNewHost()); verify(mockPluginService, atLeastOnce()).forceRefreshHostList(any(Connection.class)); - verify(mockPluginService, times(1)).setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); // 5s is a max allowed failover timeout; add 1s for inaccurate measurements assertTrue(TimeUnit.NANOSECONDS.toMillis(durationNano) < 6000); @@ -384,8 +378,6 @@ public void testFailedToConnect_taskAException_taskBWriterException() throws SQL assertFalse(result.isConnected()); assertFalse(result.isNewHost()); - verify(mockPluginService, times(1)) - .setAvailability(eq(writer.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); verify(mockPluginService, atLeastOnce()) .setAvailability(eq(newWriterHost.asAliases()), eq(HostAvailability.NOT_AVAILABLE)); } 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 551f21173..becdc7845 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 @@ -62,7 +62,6 @@ import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; import software.amazon.jdbc.hostlistprovider.DynamicHostListProvider; -import software.amazon.jdbc.states.SessionDirtyFlag; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.SqlState; import software.amazon.jdbc.util.telemetry.GaugeCallable; @@ -112,7 +111,6 @@ void init() throws SQLException { when(mockPluginService.getCurrentHostSpec()).thenReturn(mockHostSpec); when(mockPluginService.connect(any(HostSpec.class), eq(properties))).thenReturn(mockConnection); when(mockPluginService.getTelemetryFactory()).thenReturn(mockTelemetryFactory); - when(mockPluginService.getCurrentConnectionState()).thenReturn(EnumSet.allOf(SessionDirtyFlag.class)); when(mockReaderFailoverHandler.failover(any(), any())).thenReturn(mockReaderResult); when(mockWriterFailoverHandler.failover(any())).thenReturn(mockWriterResult); @@ -195,35 +193,6 @@ void test_updateTopology_withForceUpdate(final boolean forceUpdate) throws SQLEx } } - @Test - void test_syncSessionState_withNullConnections() throws SQLException { - initializePlugin(); - - plugin.transferSessionState(null, null, mockConnection, null); - verify(mockConnection, never()).getAutoCommit(); - - plugin.transferSessionState(mockConnection, null, null, null); - verify(mockConnection, never()).getAutoCommit(); - } - - @Test - void test_syncSessionState() throws SQLException { - final Connection target = mockConnection; - final Connection source = mockConnection; - - when(target.getAutoCommit()).thenReturn(false); - when(target.getTransactionIsolation()).thenReturn(Connection.TRANSACTION_NONE); - - initializePlugin(); - - plugin.transferSessionState(mockConnection, null, mockConnection, null); - verify(target).setReadOnly(eq(false)); - verify(target).getAutoCommit(); - verify(target).getTransactionIsolation(); - verify(source).setAutoCommit(eq(false)); - verify(source).setTransactionIsolation(eq(Connection.TRANSACTION_NONE)); - } - @Test void test_failover_failoverReader() throws SQLException { when(mockPluginService.isInTransaction()).thenReturn(true); diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactoryTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactoryTest.java new file mode 100644 index 000000000..d3ea267ec --- /dev/null +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/AdfsCredentialsProviderFactoryTest.java @@ -0,0 +1,113 @@ +/* + * 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.plugin.federatedauth; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.Properties; +import java.util.function.Supplier; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; + +class AdfsCredentialsProviderFactoryTest { + + private static final String USERNAME = "someFederatedUsername@example.com"; + private static final String PASSWORD = "somePassword"; + @Mock private PluginService mockPluginService; + @Mock private TelemetryFactory mockTelemetryFactory; + @Mock private TelemetryContext mockTelemetryContext; + @Mock private Supplier mockHttpClientSupplier; + @Mock private CloseableHttpClient mockHttpClient; + @Mock private CloseableHttpResponse mockHttpGetSignInPageResponse; + @Mock private CloseableHttpResponse mockHttpPostSignInResponse; + @Mock private StatusLine mockStatusLine; + @Mock private HttpEntity mockSignInPageHttpEntity; + @Mock private HttpEntity mockSamlHttpEntity; + private AdfsCredentialsProviderFactory adfsCredentialsProviderFactory; + private Properties props; + + @BeforeEach + public void init() throws IOException { + MockitoAnnotations.openMocks(this); + + this.props = new Properties(); + this.props.setProperty(FederatedAuthPlugin.IDP_ENDPOINT.name, "ec2amaz-ab3cdef.example.com"); + this.props.setProperty(FederatedAuthPlugin.IDP_USERNAME.name, USERNAME); + this.props.setProperty(FederatedAuthPlugin.IDP_PASSWORD.name, PASSWORD); + + when(mockPluginService.getTelemetryFactory()).thenReturn(mockTelemetryFactory); + when(mockTelemetryFactory.openTelemetryContext(any(), any())).thenReturn(mockTelemetryContext); + when(mockHttpClientSupplier.get()).thenReturn(mockHttpClient); + when(mockHttpClient.execute(any(HttpGet.class))).thenReturn(mockHttpGetSignInPageResponse); + when(mockHttpGetSignInPageResponse.getStatusLine()).thenReturn(mockStatusLine); + when(mockStatusLine.getStatusCode()).thenReturn(200); + when(mockHttpGetSignInPageResponse.getEntity()).thenReturn(mockSignInPageHttpEntity); + + String signinPageHtml = IOUtils.toString( + this.getClass().getClassLoader().getResourceAsStream("federated_auth/adfs-sign-in-page.html"), "UTF-8"); + InputStream signInPageHtmlInputStream = new ByteArrayInputStream(signinPageHtml.getBytes()); + when(mockSignInPageHttpEntity.getContent()).thenReturn(signInPageHtmlInputStream); + + when(mockHttpClient.execute(any(HttpPost.class))).thenReturn(mockHttpPostSignInResponse); + when(mockHttpPostSignInResponse.getStatusLine()).thenReturn(mockStatusLine); + when(mockHttpPostSignInResponse.getEntity()).thenReturn(mockSamlHttpEntity); + + String adfsSamlHtml = IOUtils.toString( + this.getClass().getClassLoader().getResourceAsStream("federated_auth/adfs-saml.html"), "UTF-8"); + InputStream samlHtmlInputStream = new ByteArrayInputStream(adfsSamlHtml.getBytes()); + when(mockSamlHttpEntity.getContent()).thenReturn(samlHtmlInputStream); + + this.adfsCredentialsProviderFactory = new AdfsCredentialsProviderFactory(mockPluginService, mockHttpClientSupplier); + } + + @Test + void test() throws IOException, SQLException { + this.adfsCredentialsProviderFactory.getSamlAssertion(props); + + ArgumentCaptor httpPostArgumentCaptor = ArgumentCaptor.forClass(HttpPost.class); + verify(mockHttpClient, times(2)).execute(httpPostArgumentCaptor.capture()); + HttpPost actualHttpPost = httpPostArgumentCaptor.getValue(); + String content = EntityUtils.toString(actualHttpPost.getEntity()); + String[] params = content.split("&"); + assertEquals("UserName=" + USERNAME.replace("@", "%40"), params[0]); + assertEquals("Password=" + PASSWORD, params[1]); + assertEquals("Kmsi=true", params[2]); + assertEquals("AuthMethod=FormsAuthentication", params[3]); + } +} diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginTest.java new file mode 100644 index 000000000..06a49d397 --- /dev/null +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/federatedauth/FederatedAuthPluginTest.java @@ -0,0 +1,188 @@ +/* + * 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.plugin.federatedauth; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Instant; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.regions.Region; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; +import software.amazon.jdbc.JdbcCallable; +import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.plugin.TokenInfo; +import software.amazon.jdbc.util.telemetry.TelemetryContext; +import software.amazon.jdbc.util.telemetry.TelemetryCounter; +import software.amazon.jdbc.util.telemetry.TelemetryFactory; + +class FederatedAuthPluginTest { + + private static final int DEFAULT_PORT = 1234; + private static final String DRIVER_PROTOCOL = "jdbc:postgresql:"; + + private static final HostSpec HOST_SPEC = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) + .host("pg.testdb.us-east-2.rds.amazonaws.com").build(); + private static final String DB_USER = "iamUser"; + private static final String TEST_TOKEN = "someTestToken"; + private static final TokenInfo TEST_TOKEN_INFO = new TokenInfo(TEST_TOKEN, Instant.now().plusMillis(300000)); + @Mock private PluginService mockPluginService; + @Mock private Dialect mockDialect; + @Mock JdbcCallable mockLambda; + @Mock private TelemetryFactory mockTelemetryFactory; + @Mock private TelemetryContext mockTelemetryContext; + @Mock private TelemetryCounter mockTelemetryCounter; + @Mock private CredentialsProviderFactory mockCredentialsProviderFactory; + @Mock private AwsCredentialsProvider mockAwsCredentialsProvider; + @Mock private CompletableFuture completableFuture; + @Mock private AwsCredentialsIdentity mockAwsCredentialsIdentity; + private Properties props; + + @BeforeEach + public void init() throws ExecutionException, InterruptedException, SQLException { + MockitoAnnotations.openMocks(this); + props = new Properties(); + props.setProperty(PropertyDefinition.PLUGINS.name, "federatedAuth"); + props.setProperty(FederatedAuthPlugin.DB_USER.name, DB_USER); + FederatedAuthPlugin.clearCache(); + + when(mockPluginService.getDialect()).thenReturn(mockDialect); + when(mockDialect.getDefaultPort()).thenReturn(DEFAULT_PORT); + when(mockPluginService.getTelemetryFactory()).thenReturn(mockTelemetryFactory); + when(mockTelemetryFactory.createCounter(any())).thenReturn(mockTelemetryCounter); + when(mockTelemetryFactory.openTelemetryContext(any(), any())).thenReturn(mockTelemetryContext); + when(mockCredentialsProviderFactory.getAwsCredentialsProvider(any(), any(), any())) + .thenReturn(mockAwsCredentialsProvider); + when(mockAwsCredentialsProvider.resolveIdentity()).thenReturn(completableFuture); + when(completableFuture.get()).thenReturn(mockAwsCredentialsIdentity); + } + + @Test + void testCachedToken() throws SQLException { + FederatedAuthPlugin plugin = + new FederatedAuthPlugin(mockPluginService, mockCredentialsProviderFactory); + + String key = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + DEFAULT_PORT + ":iamUser"; + FederatedAuthPlugin.tokenCache.put(key, TEST_TOKEN_INFO); + + plugin.connect(DRIVER_PROTOCOL, HOST_SPEC, props, true, mockLambda); + + assertEquals(DB_USER, PropertyDefinition.USER.getString(props)); + assertEquals(TEST_TOKEN, PropertyDefinition.PASSWORD.getString(props)); + } + + @Test + void testExpiredCachedToken() throws SQLException { + FederatedAuthPlugin spyPlugin = Mockito.spy( + new FederatedAuthPlugin(mockPluginService, mockCredentialsProviderFactory)); + + String key = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + DEFAULT_PORT + ":iamUser"; + String someExpiredToken = "someExpiredToken"; + TokenInfo expiredTokenInfo = new TokenInfo( + someExpiredToken, Instant.now().minusMillis(300000)); + FederatedAuthPlugin.tokenCache.put(key, expiredTokenInfo); + + when( + spyPlugin.generateAuthenticationToken( + props, + HOST_SPEC.getHost(), + DEFAULT_PORT, + Region.US_EAST_2, mockAwsCredentialsProvider)) + .thenReturn(TEST_TOKEN); + + spyPlugin.connect(DRIVER_PROTOCOL, HOST_SPEC, props, true, mockLambda); + assertEquals(DB_USER, PropertyDefinition.USER.getString(props)); + assertEquals(TEST_TOKEN, PropertyDefinition.PASSWORD.getString(props)); + } + + @Test + void testNoCachedToken() throws SQLException { + FederatedAuthPlugin spyPlugin = Mockito.spy( + new FederatedAuthPlugin(mockPluginService, mockCredentialsProviderFactory)); + + when( + spyPlugin.generateAuthenticationToken( + props, + HOST_SPEC.getHost(), + DEFAULT_PORT, + Region.US_EAST_2, mockAwsCredentialsProvider)) + .thenReturn(TEST_TOKEN); + + spyPlugin.connect(DRIVER_PROTOCOL, HOST_SPEC, props, true, mockLambda); + assertEquals(DB_USER, PropertyDefinition.USER.getString(props)); + assertEquals(TEST_TOKEN, PropertyDefinition.PASSWORD.getString(props)); + } + + @Test + void testSpecifiedIamHostPortRegion() throws SQLException { + final String expectedHost = "pg.testdb.us-west-2.rds.amazonaws.com"; + final int expectedPort = 9876; + final Region expectedRegion = Region.US_WEST_2; + + props.setProperty(FederatedAuthPlugin.IAM_HOST.name, expectedHost); + props.setProperty(FederatedAuthPlugin.IAM_DEFAULT_PORT.name, String.valueOf(expectedPort)); + props.setProperty(FederatedAuthPlugin.IAM_REGION.name, expectedRegion.toString()); + + final String key = "us-west-2:pg.testdb.us-west-2.rds.amazonaws.com:" + String.valueOf(expectedPort) + ":iamUser"; + FederatedAuthPlugin.tokenCache.put(key, TEST_TOKEN_INFO); + + FederatedAuthPlugin plugin = + new FederatedAuthPlugin(mockPluginService, mockCredentialsProviderFactory); + + plugin.connect(DRIVER_PROTOCOL, HOST_SPEC, props, true, mockLambda); + + assertEquals(DB_USER, PropertyDefinition.USER.getString(props)); + assertEquals(TEST_TOKEN, PropertyDefinition.PASSWORD.getString(props)); + } + + @Test + void testIdpCredentialsFallback() throws SQLException { + String expectedUser = "expectedUser"; + String expectedPassword = "expectedPassword"; + PropertyDefinition.USER.set(props, expectedUser); + PropertyDefinition.PASSWORD.set(props, expectedPassword); + + FederatedAuthPlugin plugin = + new FederatedAuthPlugin(mockPluginService, mockCredentialsProviderFactory); + + String key = "us-east-2:pg.testdb.us-east-2.rds.amazonaws.com:" + DEFAULT_PORT + ":iamUser"; + FederatedAuthPlugin.tokenCache.put(key, TEST_TOKEN_INFO); + + plugin.connect(DRIVER_PROTOCOL, HOST_SPEC, props, true, mockLambda); + + assertEquals(DB_USER, PropertyDefinition.USER.getString(props)); + assertEquals(TEST_TOKEN, PropertyDefinition.PASSWORD.getString(props)); + assertEquals(expectedUser, FederatedAuthPlugin.IDP_USERNAME.getString(props)); + assertEquals(expectedPassword, FederatedAuthPlugin.IDP_PASSWORD.getString(props)); + } +} 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 18ba25f21..40aa20c38 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 @@ -58,7 +58,6 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; -import software.amazon.jdbc.states.SessionDirtyFlag; import software.amazon.jdbc.util.SqlState; public class ReadWriteSplittingPluginTest { @@ -141,7 +140,6 @@ void mockDefaultBehavior() throws SQLException { when(this.mockPluginService.connect(eq(readerHostSpec3), any(Properties.class))) .thenReturn(mockReaderConn3); when(this.mockPluginService.acceptsStrategy(any(), eq("random"))).thenReturn(true); - when(mockPluginService.getCurrentConnectionState()).thenReturn(EnumSet.allOf(SessionDirtyFlag.class)); when(this.mockConnectFunc.call()).thenReturn(mockWriterConn); when(mockWriterConn.createStatement()).thenReturn(mockStatement); when(mockReaderConn1.createStatement()).thenReturn(mockStatement); diff --git a/wrapper/src/test/java/software/amazon/jdbc/states/SessionStateServiceImplTests.java b/wrapper/src/test/java/software/amazon/jdbc/states/SessionStateServiceImplTests.java new file mode 100644 index 000000000..24565f5d0 --- /dev/null +++ b/wrapper/src/test/java/software/amazon/jdbc/states/SessionStateServiceImplTests.java @@ -0,0 +1,417 @@ +/* + * 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.states; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.jdbc.PluginService; + +public class SessionStateServiceImplTests { + + @Mock PluginService mockPluginService; + @Mock Connection mockConnection; + @Mock Connection mockNewConnection; + Properties props = new Properties(); + SessionStateService sessionStateService; + private AutoCloseable closeable; + + @Captor ArgumentCaptor captorReadOnly; + @Captor ArgumentCaptor captorAutoCommit; + @Captor ArgumentCaptor captorCatalog; + @Captor ArgumentCaptor captorSchema; + @Captor ArgumentCaptor captorHoldability; + @Captor ArgumentCaptor captorNetworkTimeout; + @Captor ArgumentCaptor captorTransactionIsolation; + @Captor ArgumentCaptor>> captorTypeMap; + + @AfterEach + void afterEach() throws Exception { + closeable.close(); + sessionStateService = null; + props.clear(); + } + + @BeforeEach + void beforeEach() throws SQLException { + closeable = MockitoAnnotations.openMocks(this); + when(mockPluginService.getCurrentConnection()).thenReturn(mockConnection); + sessionStateService = spy(new SessionStateServiceImpl(mockPluginService, props)); + } + + @ParameterizedTest + @MethodSource("getBoolArguments") + public void test_ResetConnection_ReadOnly( + boolean pristineValue, boolean value, boolean shouldReset) throws SQLException { + + when(mockConnection.isReadOnly()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getReadOnly()); + sessionStateService.setupPristineReadOnly(); + sessionStateService.setReadOnly(value); + assertEquals(Optional.of(value), sessionStateService.getReadOnly()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setReadOnly(captorReadOnly.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorReadOnly.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getBoolArguments") + public void test_ResetConnection_AutoCommit( + boolean pristineValue, boolean value, boolean shouldReset) throws SQLException { + + when(mockConnection.getAutoCommit()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getAutoCommit()); + sessionStateService.setupPristineAutoCommit(); + sessionStateService.setAutoCommit(value); + assertEquals(Optional.of(value), sessionStateService.getAutoCommit()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setAutoCommit(captorAutoCommit.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorAutoCommit.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getStringArguments") + public void test_ResetConnection_Catalog( + String pristineValue, String value, boolean shouldReset) throws SQLException { + + when(mockConnection.getCatalog()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getCatalog()); + sessionStateService.setupPristineCatalog(); + sessionStateService.setCatalog(value); + assertEquals(Optional.of(value), sessionStateService.getCatalog()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setCatalog(captorCatalog.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorCatalog.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getStringArguments") + public void test_ResetConnection_Schema( + String pristineValue, String value, boolean shouldReset) throws SQLException { + + when(mockConnection.getSchema()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getSchema()); + sessionStateService.setupPristineSchema(); + sessionStateService.setSchema(value); + assertEquals(Optional.of(value), sessionStateService.getSchema()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setSchema(captorSchema.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorSchema.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_ResetConnection_Holdability( + int pristineValue, int value, boolean shouldReset) throws SQLException { + + when(mockConnection.getHoldability()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getHoldability()); + sessionStateService.setupPristineHoldability(); + sessionStateService.setHoldability(value); + assertEquals(Optional.of(value), sessionStateService.getHoldability()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setHoldability(captorHoldability.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorHoldability.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_ResetConnection_NetworkTimeout( + int pristineValue, int value, boolean shouldReset) throws SQLException { + + when(mockConnection.getNetworkTimeout()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getNetworkTimeout()); + sessionStateService.setupPristineNetworkTimeout(); + sessionStateService.setNetworkTimeout(value); + assertEquals(Optional.of(value), sessionStateService.getNetworkTimeout()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setNetworkTimeout(any(), captorNetworkTimeout.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorNetworkTimeout.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_ResetConnection_TransactionIsolation( + int pristineValue, int value, boolean shouldReset) throws SQLException { + + when(mockConnection.getTransactionIsolation()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getTransactionIsolation()); + sessionStateService.setupPristineTransactionIsolation(); + sessionStateService.setTransactionIsolation(value); + assertEquals(Optional.of(value), sessionStateService.getTransactionIsolation()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)) + .setTransactionIsolation(captorTransactionIsolation.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorTransactionIsolation.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getTypeMapArguments") + public void test_ResetConnection_TypeMap( + Map> pristineValue, Map> value, boolean shouldReset) throws SQLException { + + when(mockConnection.getTypeMap()).thenReturn(pristineValue); + assertEquals(Optional.empty(), sessionStateService.getTypeMap()); + sessionStateService.setupPristineTypeMap(); + sessionStateService.setTypeMap(value); + assertEquals(Optional.of(value), sessionStateService.getTypeMap()); + + sessionStateService.begin(); + sessionStateService.applyPristineSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(shouldReset ? 1 : 0)).setTypeMap(captorTypeMap.capture()); + if (shouldReset) { + assertEquals(pristineValue, captorTypeMap.getValue()); + } + } + + @ParameterizedTest + @MethodSource("getBoolArguments") + public void test_TransferToNewConnection_ReadOnly(boolean pristineValue, boolean value) throws SQLException { + when(mockConnection.isReadOnly()).thenReturn(pristineValue); + when(mockNewConnection.isReadOnly()).thenReturn(pristineValue); + sessionStateService.setReadOnly(value); + assertEquals(Optional.of(value), sessionStateService.getReadOnly()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setReadOnly(captorReadOnly.capture()); + assertEquals(value, captorReadOnly.getValue()); + } + + @ParameterizedTest + @MethodSource("getBoolArguments") + public void test_TransferToNewConnection_AutoCommit(boolean pristineValue, boolean value) throws SQLException { + when(mockConnection.getAutoCommit()).thenReturn(pristineValue); + when(mockNewConnection.getAutoCommit()).thenReturn(pristineValue); + sessionStateService.setAutoCommit(value); + assertEquals(Optional.of(value), sessionStateService.getAutoCommit()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setAutoCommit(captorAutoCommit.capture()); + assertEquals(value, captorAutoCommit.getValue()); + } + + @ParameterizedTest + @MethodSource("getStringArguments") + public void test_TransferToNewConnection_Catalog(String pristineValue, String value) throws SQLException { + when(mockConnection.getCatalog()).thenReturn(pristineValue); + when(mockNewConnection.getCatalog()).thenReturn(pristineValue); + sessionStateService.setCatalog(value); + assertEquals(Optional.of(value), sessionStateService.getCatalog()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setCatalog(captorCatalog.capture()); + assertEquals(value, captorCatalog.getValue()); + } + + @ParameterizedTest + @MethodSource("getStringArguments") + public void test_TransferToNewConnection_Schema(String pristineValue, String value) throws SQLException { + when(mockConnection.getSchema()).thenReturn(pristineValue); + when(mockNewConnection.getSchema()).thenReturn(pristineValue); + sessionStateService.setSchema(value); + assertEquals(Optional.of(value), sessionStateService.getSchema()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setSchema(captorSchema.capture()); + assertEquals(value, captorSchema.getValue()); + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_TransferToNewConnection_Holdability(int pristineValue, int value) throws SQLException { + when(mockConnection.getHoldability()).thenReturn(pristineValue); + when(mockNewConnection.getHoldability()).thenReturn(pristineValue); + sessionStateService.setHoldability(value); + assertEquals(Optional.of(value), sessionStateService.getHoldability()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setHoldability(captorHoldability.capture()); + assertEquals(value, captorHoldability.getValue()); + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_TransferToNewConnection_NetworkTimeout(int pristineValue, int value) throws SQLException { + when(mockConnection.getNetworkTimeout()).thenReturn(pristineValue); + when(mockNewConnection.getNetworkTimeout()).thenReturn(pristineValue); + sessionStateService.setNetworkTimeout(value); + assertEquals(Optional.of(value), sessionStateService.getNetworkTimeout()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setNetworkTimeout(any(), captorNetworkTimeout.capture()); + assertEquals(value, captorNetworkTimeout.getValue()); + } + + @ParameterizedTest + @MethodSource("getIntegerArguments") + public void test_TransferToNewConnection_TransactionIsolation(int pristineValue, int value) throws SQLException { + when(mockConnection.getTransactionIsolation()).thenReturn(pristineValue); + when(mockNewConnection.getTransactionIsolation()).thenReturn(pristineValue); + sessionStateService.setTransactionIsolation(value); + assertEquals(Optional.of(value), sessionStateService.getTransactionIsolation()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)) + .setTransactionIsolation(captorTransactionIsolation.capture()); + assertEquals(value, captorTransactionIsolation.getValue()); + } + + @ParameterizedTest + @MethodSource("getTypeMapArguments") + public void test_TransferToNewConnection_TypeMap( + Map> pristineValue, Map> value) throws SQLException { + + when(mockConnection.getTypeMap()).thenReturn(pristineValue); + when(mockNewConnection.getTypeMap()).thenReturn(pristineValue); + sessionStateService.setTypeMap(value); + assertEquals(Optional.of(value), sessionStateService.getTypeMap()); + + sessionStateService.begin(); + sessionStateService.applyCurrentSessionState(mockNewConnection); + sessionStateService.complete(); + + verify(mockNewConnection, times(1)).setTypeMap(captorTypeMap.capture()); + assertEquals(value, captorTypeMap.getValue()); + } + + static Stream getBoolArguments() { + return Stream.of( + Arguments.of(false, false, false), + Arguments.of(true, false, true), + Arguments.of(false, true, true), + Arguments.of(true, true, false) + ); + } + + static Stream getStringArguments() { + return Stream.of( + Arguments.of("a", "a", false), + Arguments.of("b", "a", true), + Arguments.of("a", "b", true), + Arguments.of("b", "b", false) + ); + } + + static Stream getIntegerArguments() { + return Stream.of( + Arguments.of(1, 1, false), + Arguments.of(2, 1, true), + Arguments.of(1, 2, true), + Arguments.of(2, 2, false)); + } + + static Stream getTypeMapArguments() { + final Map> a1 = new HashMap<>(); + final Map> a2 = new HashMap<>(); + final Map> b1 = new HashMap<>(); + b1.put("test", Object.class); // actual mapping isn't important here + final Map> b2 = new HashMap<>(); + b2.put("test", Object.class); // actual mapping isn't important here + + return Stream.of( + Arguments.of(a1, a2, false), + Arguments.of(b1, a2, true), + Arguments.of(a1, b2, true), + Arguments.of(b1, b2, false)); + } +} diff --git a/wrapper/src/test/java/software/amazon/jdbc/util/SqlMethodAnalyzerTest.java b/wrapper/src/test/java/software/amazon/jdbc/util/SqlMethodAnalyzerTest.java index fb93cba8a..93faf55dc 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/util/SqlMethodAnalyzerTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/util/SqlMethodAnalyzerTest.java @@ -16,6 +16,7 @@ package software.amazon.jdbc.util; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,6 +35,8 @@ import org.mockito.MockitoAnnotations; class SqlMethodAnalyzerTest { + private static final String EXECUTE_METHOD = "execute"; + private static final String EMPTY_SQL = ""; @Mock Connection conn; @@ -68,6 +71,11 @@ void testOpenTransaction(final String methodName, final String sql, final boolea assertEquals(expected, actual); } + @Test + void testOpenTransactionWithEmptySqlDoesNotThrow() { + assertDoesNotThrow(() -> sqlMethodAnalyzer.doesOpenTransaction(conn, EXECUTE_METHOD, new String[]{EMPTY_SQL})); + } + @ParameterizedTest @MethodSource("closeTransactionQueries") void testCloseTransaction(final String methodName, final String sql, final boolean expected) { @@ -82,6 +90,11 @@ void testCloseTransaction(final String methodName, final String sql, final boole assertEquals(expected, actual); } + @Test + void testCloseTransactionWithEmptySqlDoesNotThrow() { + assertDoesNotThrow(() -> sqlMethodAnalyzer.doesCloseTransaction(conn, EXECUTE_METHOD, new String[]{EMPTY_SQL})); + } + @Test void testDoesSwitchAutoCommitFalseTrue() throws SQLException { assertFalse(sqlMethodAnalyzer.doesSwitchAutoCommitFalseTrue(conn, "Connection.setAutoCommit", @@ -123,6 +136,11 @@ void testIsStatementSettingAutoCommit(final String methodName, final String sql, assertEquals(expected, actual); } + @Test + void testIsStatementSettingAutoCommitWithEmptySqlDoesNotThrow() { + assertDoesNotThrow(() -> sqlMethodAnalyzer.isStatementSettingAutoCommit(EXECUTE_METHOD, new String[]{EMPTY_SQL})); + } + @ParameterizedTest @MethodSource("getAutoCommitQueries") void testGetAutoCommit(final String sql, final Boolean expected) { @@ -137,6 +155,11 @@ void testGetAutoCommit(final String sql, final Boolean expected) { assertEquals(expected, actual); } + @Test + void testGetAutoCommitWithEmptySqlDoesNotThrow() { + assertDoesNotThrow(() -> sqlMethodAnalyzer.getAutoCommitValueFromSqlStatement(new String[]{EMPTY_SQL})); + } + @ParameterizedTest @MethodSource("getIsMethodClosingSqlObjectMethods") void testIsMethodClosingSqlObject(final String methodName, final boolean expected) { diff --git a/wrapper/src/test/resources/federated_auth/adfs-saml.html b/wrapper/src/test/resources/federated_auth/adfs-saml.html new file mode 100644 index 000000000..6686e3db8 --- /dev/null +++ b/wrapper/src/test/resources/federated_auth/adfs-saml.html @@ -0,0 +1 @@ +Working...
diff --git a/wrapper/src/test/resources/federated_auth/adfs-sign-in-page.html b/wrapper/src/test/resources/federated_auth/adfs-sign-in-page.html new file mode 100644 index 000000000..0b06f6dda --- /dev/null +++ b/wrapper/src/test/resources/federated_auth/adfs-sign-in-page.html @@ -0,0 +1,613 @@ + + + + + + + + + + + + Sign In + + + + + + + + + + +
+

JavaScript required

+

JavaScript is required. This web browser does not support JavaScript or JavaScript in this web browser is not enabled.

+

To find out if your web browser supports JavaScript or to enable JavaScript, see web browser help.

+
+ +
+
+
+
+
+
+ +
+
+ +
+ + + +
+
Sign in
+ +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ Sign in +
+
+ +
+ +
+
+ + + + +
+
+ +
+ +
+ + +
+ +
+ +
+
+
+
+
+ +
+
+
+ + + + + + diff --git a/wrapper/src/test/resources/hibernate_files/hibernate-core.gradle b/wrapper/src/test/resources/hibernate_files/hibernate-core.gradle index 0089cc4d4..b12d49169 100644 --- a/wrapper/src/test/resources/hibernate_files/hibernate-core.gradle +++ b/wrapper/src/test/resources/hibernate_files/hibernate-core.gradle @@ -61,7 +61,7 @@ dependencies { transitive = true } testImplementation "joda-time:joda-time:2.3" - testImplementation files('/app/libs/aws-advanced-jdbc-wrapper-2.3.0.jar') + testImplementation files('/app/libs/aws-advanced-jdbc-wrapper-2.3.2.jar') testImplementation dbLibs.postgresql testImplementation dbLibs.mysql testImplementation dbLibs.h2 diff --git a/wrapper/src/test/resources/hibernate_files/java-module.gradle b/wrapper/src/test/resources/hibernate_files/java-module.gradle index f8e425a42..f73b85ecb 100644 --- a/wrapper/src/test/resources/hibernate_files/java-module.gradle +++ b/wrapper/src/test/resources/hibernate_files/java-module.gradle @@ -97,7 +97,7 @@ dependencies { // Since both the DB2 driver and HANA have a package "net.jpountz" we have to add dependencies conditionally // This is due to the "no split-packages" requirement of Java 9+ - testRuntimeOnly files('/app/libs/aws-advanced-jdbc-wrapper-2.3.0.jar') + testRuntimeOnly files('/app/libs/aws-advanced-jdbc-wrapper-2.3.2.jar') testRuntimeOnly dbLibs.mysql if ( db.startsWith( 'db2' ) ) { diff --git a/wrapper/src/test/resources/simplelogger.properties b/wrapper/src/test/resources/simplelogger.properties index 21e3e39ec..175a1da3b 100644 --- a/wrapper/src/test/resources/simplelogger.properties +++ b/wrapper/src/test/resources/simplelogger.properties @@ -22,5 +22,3 @@ org.slf4j.simpleLogger.defaultLogLevel=warn org.slf4j.simpleLogger.log.org.testcontainers=warn org.slf4j.simpleLogger.log.com.github.dockerjava=info org.slf4j.simpleLogger.log.integration.container=debug - -org.slf4j.simpleLogger.log.com.zaxxer.hikari=trace