From e8ae208f87342641e5c2eedf7f8cf2ad4214ca6c Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Fri, 13 Jan 2023 10:36:10 -0500 Subject: [PATCH] Changes to enable Aurora failover logic --- .github/pull_request_template.md | 21 + .github/workflows/dockerized.yml | 226 +++++ .github/workflows/failover.yml | 260 ++++++ .github/workflows/main.yml | 204 +++++ .github/workflows/performance.yml | 157 ++++ .github/workflows/release.yml | 178 ++++ .github/workflows/remove-old-artifacts.yml | 18 + .gitignore | 86 +- CMakeLists.txt | 90 +- CreateBinaryMsi.bat | 2 + GoogleTest.LICENSE | 28 + Install.bat.cmake | 2 + MYODBC_MYSQL.h | 4 +- MYODBC_ODBC.h | 59 +- README.md | 787 +++++++++++++++++- README.txt | 35 +- build_installer.ps1 | 88 ++ docs/images/efm_monitor_process.png | Bin 0 -> 40047 bytes docs/images/enable_logging_windows.jpg | Bin 0 -> 34355 bytes .../enhanced_failure_monitoring_diagram.png | Bin 0 -> 50709 bytes docs/images/failover_behaviour.jpg | Bin 0 -> 393459 bytes docs/images/failover_diagram.png | Bin 0 -> 48011 bytes driver/CMakeLists.txt | 33 +- driver/ansi.cc | 21 +- driver/base_metrics_holder.cc | 200 +++++ driver/base_metrics_holder.h | 78 ++ driver/catalog.cc | 61 +- driver/catalog.h | 2 + driver/catalog_no_i_s.cc | 100 ++- driver/cluster_aware_hit_metrics_holder.cc | 56 ++ driver/cluster_aware_hit_metrics_holder.h | 48 ++ driver/cluster_aware_metrics.cc | 74 ++ driver/cluster_aware_metrics.h | 62 ++ driver/cluster_aware_metrics_container.cc | 162 ++++ driver/cluster_aware_metrics_container.h | 89 ++ driver/cluster_aware_time_metrics_holder.cc | 108 +++ driver/cluster_aware_time_metrics_holder.h | 47 ++ driver/cluster_topology_info.cc | 160 ++++ driver/cluster_topology_info.h | 86 ++ driver/connect.cc | 292 ++++--- driver/cursor.cc | 70 +- driver/dll.cc | 8 +- driver/driver.def.cmake | 2 + driver/driver.h | 119 +-- driver/driver.rc.cmake | 2 + driver/error.cc | 44 +- driver/error.h | 12 +- driver/execute.cc | 153 ++-- driver/failover.h | 323 +++++++ driver/failover_connection_handler.cc | 127 +++ driver/failover_handler.cc | 522 ++++++++++++ driver/failover_reader_handler.cc | 271 ++++++ driver/failover_writer_handler.cc | 353 ++++++++ driver/handle.cc | 65 +- driver/host_info.cc | 118 +++ driver/host_info.h | 78 ++ driver/info.cc | 42 +- driver/monitor.cc | 237 ++++++ driver/monitor.h | 101 +++ driver/monitor_connection_context.cc | 209 +++++ driver/monitor_connection_context.h | 104 +++ driver/monitor_service.cc | 145 ++++ driver/monitor_service.h | 63 ++ driver/monitor_thread_container.cc | 226 +++++ driver/monitor_thread_container.h | 92 ++ driver/my_prepared_stmt.cc | 67 +- driver/my_stmt.cc | 85 +- driver/mylog.cc | 125 +++ driver/mylog.h | 70 ++ driver/mysql_proxy.cc | 631 ++++++++++++++ driver/mysql_proxy.h | 196 +++++ driver/myutil.h | 38 +- driver/options.cc | 57 +- driver/parse.cc | 30 +- driver/prepare.cc | 4 +- driver/query_parsing.cc | 155 ++++ driver/query_parsing.h | 38 + driver/results.cc | 36 +- driver/topology_service.cc | 336 ++++++++ driver/topology_service.h | 121 +++ driver/transact.cc | 18 +- driver/unicode.cc | 8 +- driver/utility.cc | 224 ++--- installer/myodbc-installer.cc | 41 +- integration/CMakeLists.txt | 138 +++ integration/base_failover_integration_test.cc | 472 +++++++++++ integration/connection_string_builder.cc | 386 +++++++++ integration/connection_string_builder_test.cc | 172 ++++ integration/failover_integration_test.cc | 413 +++++++++ integration/failover_performance_test.cc | 449 ++++++++++ .../network_failover_integration_test.cc | 308 +++++++ mysql_strings/conf_to_src.cc | 4 +- mysql_strings/ctype.cc | 10 +- mysql_strings/uca-dump.cc | 2 +- mysql_strings/uca9-dump.cc | 2 +- mysql_strings/uctypedump.cc | 6 +- mysql_strings/xml.cc | 17 +- packaging/debian/CMakeLists.txt | 2 + .../mysql-connector-odbc-setup.postinst.in | 2 + .../mysql-connector-odbc-setup.prerm.in | 2 + .../debian/mysql-connector-odbc.postinst.in | 2 + scripts/macosx/ReadMe.html.in | 20 +- scripts/macosx/postflight.in | 6 +- setupgui/CMakeLists.txt | 4 +- setupgui/ConfigDSN.cc | 6 +- setupgui/callbacks.cc | 135 ++- setupgui/gtk/ODBCINSTGetProperties.cc | 4 +- setupgui/gtk/odbc.glade | 4 +- setupgui/gtk/odbcdialogparams.cc | 4 +- setupgui/gtk/setup_loader.cc | 2 + setupgui/gtk/ui_xml.h | 6 +- setupgui/gtk/ui_xml_v2.h | 6 +- setupgui/setupgui.h | 26 +- setupgui/utils.cc | 57 +- setupgui/windows/odbcdialogparams.cpp | 61 +- setupgui/windows/odbcdialogparams.rc | 107 ++- setupgui/windows/resource.h | 79 +- setupgui/windows/tooltip.cpp | Bin 13238 -> 6504 bytes test/CMakeLists.txt | 18 +- test/docker/docker-compose.yml | 10 + test/my_auth.cc | 4 +- test/my_basics.c | 37 +- test/my_blob.c | 14 +- test/my_bug13766.c | 7 +- test/my_bulk.c | 10 +- test/my_catalog1.c | 46 +- test/my_catalog2.c | 108 ++- test/my_crash.c | 153 ++-- test/my_curext.c | 7 +- test/my_cursor.c | 42 +- test/my_data.cc | 3 +- test/my_datetime.c | 14 +- test/my_desc.c | 10 +- test/my_dyn_cursor.c | 8 +- test/my_error.c | 62 +- test/my_info.c | 53 +- test/my_keys.c | 5 +- test/my_options.cc | 53 +- test/my_param.c | 164 ++-- test/my_pooling.c | 6 +- test/my_prepare.c | 52 +- test/my_relative.c | 5 +- test/my_result1.c | 29 +- test/my_result2.c | 80 +- test/my_scroll.c | 7 +- test/my_setup.c | 41 +- test/my_types.c | 24 +- test/my_unicode.c | 6 +- test/my_unixodbc.c | 6 +- test/my_use_result.c | 4 +- test/odbctap.h | 126 ++- testframework/build.gradle.kts | 39 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + testframework/gradlew | 185 ++++ testframework/gradlew.bat | 89 ++ testframework/settings.gradle.kts | 2 + .../java/host/IntegrationContainerTest.java | 232 ++++++ .../test/java/utility/AuroraClusterInfo.java | 66 ++ .../test/java/utility/AuroraTestUtility.java | 321 +++++++ .../test/java/utility/ConsoleConsumer.java | 70 ++ .../test/java/utility/ContainerHelper.java | 213 +++++ testframework/src/test/resources/odbc.ini.in | 11 + .../src/test/resources/odbcinst.ini.in | 12 + unit_testing/CMakeLists.txt | 82 ++ unit_testing/cluster_aware_metrics_test.cc | 151 ++++ unit_testing/failover_handler_test.cc | 389 +++++++++ unit_testing/failover_reader_handler_test.cc | 422 ++++++++++ unit_testing/failover_writer_handler_test.cc | 492 +++++++++++ unit_testing/main.cc | 57 ++ unit_testing/mock_objects.h | 205 +++++ .../monitor_connection_context_test.cc | 129 +++ unit_testing/monitor_service_test.cc | 181 ++++ unit_testing/monitor_test.cc | 352 ++++++++ unit_testing/monitor_thread_container_test.cc | 270 ++++++ .../multi_threaded_monitor_service_test.cc | 225 +++++ unit_testing/mysql_proxy_test.cc | 101 +++ unit_testing/query_parsing_test.cc | 67 ++ unit_testing/test_utils.cc | 112 +++ unit_testing/test_utils.h | 55 ++ unit_testing/topology_service_test.cc | 292 +++++++ util/CMakeLists.txt | 2 + util/installer.cc | 408 ++++++++- util/installer.h | 64 +- util/stringutil.cc | 35 +- util/stringutil.h | 5 +- version.cmake | 2 + 187 files changed, 17493 insertions(+), 1668 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/dockerized.yml create mode 100644 .github/workflows/failover.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/performance.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/remove-old-artifacts.yml create mode 100644 GoogleTest.LICENSE create mode 100644 build_installer.ps1 create mode 100644 docs/images/efm_monitor_process.png create mode 100644 docs/images/enable_logging_windows.jpg create mode 100644 docs/images/enhanced_failure_monitoring_diagram.png create mode 100644 docs/images/failover_behaviour.jpg create mode 100644 docs/images/failover_diagram.png create mode 100644 driver/base_metrics_holder.cc create mode 100644 driver/base_metrics_holder.h create mode 100644 driver/cluster_aware_hit_metrics_holder.cc create mode 100644 driver/cluster_aware_hit_metrics_holder.h create mode 100644 driver/cluster_aware_metrics.cc create mode 100644 driver/cluster_aware_metrics.h create mode 100644 driver/cluster_aware_metrics_container.cc create mode 100644 driver/cluster_aware_metrics_container.h create mode 100644 driver/cluster_aware_time_metrics_holder.cc create mode 100644 driver/cluster_aware_time_metrics_holder.h create mode 100644 driver/cluster_topology_info.cc create mode 100644 driver/cluster_topology_info.h mode change 100755 => 100644 driver/connect.cc mode change 100755 => 100644 driver/execute.cc create mode 100644 driver/failover.h create mode 100644 driver/failover_connection_handler.cc create mode 100644 driver/failover_handler.cc create mode 100644 driver/failover_reader_handler.cc create mode 100644 driver/failover_writer_handler.cc create mode 100644 driver/host_info.cc create mode 100644 driver/host_info.h create mode 100644 driver/monitor.cc create mode 100644 driver/monitor.h create mode 100644 driver/monitor_connection_context.cc create mode 100644 driver/monitor_connection_context.h create mode 100644 driver/monitor_service.cc create mode 100644 driver/monitor_service.h create mode 100644 driver/monitor_thread_container.cc create mode 100644 driver/monitor_thread_container.h create mode 100644 driver/mylog.cc create mode 100644 driver/mylog.h create mode 100644 driver/mysql_proxy.cc create mode 100644 driver/mysql_proxy.h create mode 100644 driver/query_parsing.cc create mode 100644 driver/query_parsing.h mode change 100755 => 100644 driver/results.cc create mode 100644 driver/topology_service.cc create mode 100644 driver/topology_service.h create mode 100644 integration/CMakeLists.txt create mode 100644 integration/base_failover_integration_test.cc create mode 100644 integration/connection_string_builder.cc create mode 100644 integration/connection_string_builder_test.cc create mode 100644 integration/failover_integration_test.cc create mode 100644 integration/failover_performance_test.cc create mode 100644 integration/network_failover_integration_test.cc create mode 100644 test/docker/docker-compose.yml create mode 100644 testframework/build.gradle.kts create mode 100644 testframework/gradle/wrapper/gradle-wrapper.jar create mode 100644 testframework/gradle/wrapper/gradle-wrapper.properties create mode 100755 testframework/gradlew create mode 100644 testframework/gradlew.bat create mode 100644 testframework/settings.gradle.kts create mode 100644 testframework/src/test/java/host/IntegrationContainerTest.java create mode 100644 testframework/src/test/java/utility/AuroraClusterInfo.java create mode 100644 testframework/src/test/java/utility/AuroraTestUtility.java create mode 100644 testframework/src/test/java/utility/ConsoleConsumer.java create mode 100644 testframework/src/test/java/utility/ContainerHelper.java create mode 100644 testframework/src/test/resources/odbc.ini.in create mode 100644 testframework/src/test/resources/odbcinst.ini.in create mode 100644 unit_testing/CMakeLists.txt create mode 100644 unit_testing/cluster_aware_metrics_test.cc create mode 100644 unit_testing/failover_handler_test.cc create mode 100644 unit_testing/failover_reader_handler_test.cc create mode 100644 unit_testing/failover_writer_handler_test.cc create mode 100644 unit_testing/main.cc create mode 100644 unit_testing/mock_objects.h create mode 100644 unit_testing/monitor_connection_context_test.cc create mode 100644 unit_testing/monitor_service_test.cc create mode 100644 unit_testing/monitor_test.cc create mode 100644 unit_testing/monitor_thread_container_test.cc create mode 100644 unit_testing/multi_threaded_monitor_service_test.cc create mode 100644 unit_testing/mysql_proxy_test.cc create mode 100644 unit_testing/query_parsing_test.cc create mode 100644 unit_testing/test_utils.cc create mode 100644 unit_testing/test_utils.h create mode 100644 unit_testing/topology_service_test.cc diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..5e6275c3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +### Jira Ticket + + + +### Review Status + + +- [ ] This is ready for review +- [ ] This is complete + +### Summary + + + +### Description + + + +### Additional Reviewers + + diff --git a/.github/workflows/dockerized.yml b/.github/workflows/dockerized.yml new file mode 100644 index 00000000..64d46ff3 --- /dev/null +++ b/.github/workflows/dockerized.yml @@ -0,0 +1,226 @@ +name: Dockerized Tests + +on: + push: + branches: + - main + - myodbc + pull_request: + branches: + - '*' + paths-ignore: + - '**/*.md' + - '**/*.jpg' + - '**/README.txt' + - 'docs/**' + - 'ISSUE_TEMPLATE/**' + - '**/remove-old-artifacts.yml' + +env: + BUILD_TYPE: Release + +jobs: + build-dockerized-community-tests: + name: Dockerized Community Tests + runs-on: ubuntu-20.04 + env: + CMAKE_GENERATOR: Unix Makefiles + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs & other dependencies + run: sudo apt-get update && sudo apt-get install + build-essential + libgtk-3-dev + libmysqlclient-dev + unixodbc + unixodbc-dev + curl + libcurl4-openssl-dev + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=TRUE + -DENABLE_INTEGRATION_TESTS=FALSE + -DENABLE_PERFORMANCE_TESTS=FALSE + -DENABLE_UNIT_TESTS=TRUE + -DWITH_UNIXODBC=1 + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + + - name: 'Set up JDK 8' + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: 'Run Community Tests' + working-directory: ${{ github.workspace }}/testframework + run: | + ./gradlew --no-parallel --no-daemon test-community --info + env: + TEST_DSN: myodbc8a + TEST_USERNAME: root + TEST_PASSWORD: root + DRIVER_PATH: ${{ github.workspace }}/build + + build-dockerized-integration-tests: + concurrency: # Cancel previous runs in the same branch + group: environment-${{ github.ref }} + cancel-in-progress: true + name: Dockerized Integration Tests + runs-on: ubuntu-20.04 + env: + CMAKE_GENERATOR: Unix Makefiles + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs & other dependencies + run: sudo apt-get update && sudo apt-get install + build-essential + libgtk-3-dev + libmysqlclient-dev + unixodbc + unixodbc-dev + curl + libcurl4-openssl-dev + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + # Performance tests are disabled by default + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=TRUE + -DENABLE_INTEGRATION_TESTS=TRUE + -DENABLE_PERFORMANCE_TESTS=FALSE + -DENABLE_UNIT_TESTS=FALSE + -DWITH_UNIXODBC=1 + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + + - name: 'Set up JDK 8' + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: 'Configure AWS Credentials' + uses: aws-actions/configure-aws-credentials@v1 + 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 3600 \ + --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 Integration Tests' + working-directory: ${{ github.workspace }}/testframework + run: | + ./gradlew --no-parallel --no-daemon test-failover --info + env: + TEST_DSN: atlas + TEST_USERNAME: ${{ secrets.TEST_USERNAME }} + TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} + TEST_DB_CLUSTER_IDENTIFIER: ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} + 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 }} + DRIVER_PATH: ${{ github.workspace }}/build + + - name: 'Get Github Action IP' + id: ip + uses: haythem/public-ip@v1.2 + + - name: 'Remove Github Action IP' + if: always() + run: | + aws ec2 revoke-security-group-ingress \ + --group-name default \ + --protocol tcp \ + --port 3306 \ + --cidr ${{ steps.ip.outputs.ipv4 }}/32 \ + 2>&1 > /dev/null; + + - name: 'Display and save log' + if: always() + working-directory: ${{ github.workspace }}/build + run: | + echo "Displaying logs" + mkdir -p ./reports/tests + if [[ -f myodbc.log && -s myodbc.log ]]; then + cat myodbc.log + cp myodbc.log ./reports/tests/myodbc.log + fi + if [[ -f failover_performance.xlsx && -s failover_performance.xlsx ]]; then + cp failover_performance.xlsx ./reports/tests/failover_performance.xlsx + fi + if [[ -f efm_performance.xlsx && -s efm_performance.xlsx ]]; then + cp efm_performance.xlsx ./reports/tests/efm_performance.xlsx + fi + if [[ -f efm_detection_performance.xlsx && -s efm_detection_performance.xlsx ]]; then + cp efm_detection_performance.xlsx ./reports/tests/efm_detection_performance.xlsx + fi + + - name: 'Archive log results' + if: always() + uses: actions/upload-artifact@v2 + with: + name: 'integration-test-logs' + path: build/reports/tests/ + retention-days: 3 + + - name: 'Delete Test Clusters if Ungraceful Cancel' + if: cancelled() + run: | + db_instances=($(aws rds describe-db-clusters \ + --db-cluster-identifier ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} \ + --query 'DBClusters[].DBClusterMembers[].DBInstanceIdentifier' \ + --output text \ + | xargs)); + for ((i = 0; i < ${#db_instances[@]}; i++)); \ + do \ + aws rds delete-db-instance \ + --db-instance-identifier ${db_instances[i]} \ + --skip-final-snapshot \ + 2>&1 > /dev/null; \ + done; + aws rds delete-db-cluster \ + --db-cluster-identifier ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} \ + --skip-final-snapshot \ + 2>&1 > /dev/null; diff --git a/.github/workflows/failover.yml b/.github/workflows/failover.yml new file mode 100644 index 00000000..d20a0817 --- /dev/null +++ b/.github/workflows/failover.yml @@ -0,0 +1,260 @@ +name: Failover Unit Tests + +on: + push: + branches: + - main + - myodbc + pull_request: + branches: + - '*' + paths-ignore: + - '**/*.md' + - '**/*.jpg' + - '**/README.txt' + - 'docs/**' + - 'ISSUE_TEMPLATE/**' + - '**/remove-old-artifacts.yml' + +env: + LINUX_BUILD_TYPE: Release + WINDOWS_BUILD_TYPE: Debug + MAC_BUILD_TYPE: Debug + +jobs: + build-windows: + name: Windows + runs-on: windows-2019 + env: + CMAKE_GENERATOR: Visual Studio 16 2019 + MYSQL_DIR: C:/mysql-8.0.30-winx64 + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install MySQL client libs and include files + if: steps.cache-mysql.outputs.cache-hit != 'true' + run: | + curl https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.30-winx64.zip -o mysql.zip + curl https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.30-winx64-debug-test.zip -o mysql-debug.zip + unzip -d C:/ mysql.zip + mkdir C:/mysql-8.0.30-winx64-debug + unzip -d C:/mysql-8.0.30-winx64-debug mysql-debug.zip + mv -Force C:/mysql-8.0.30-winx64-debug/mysql-8.0.30-winx64/lib/debug/mysqlclient.lib C:/mysql-8.0.30-winx64/lib/mysqlclient.lib + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build -A x64 + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$WINDOWS_BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=TRUE + -DENABLE_UNIT_TESTS=TRUE + -DENABLE_INTEGRATION_TESTS=FALSE + + # Configure test environment + - name: Build Driver + shell: bash + working-directory: ${{ github.workspace }}/build + run: | + cmake --build . --config $WINDOWS_BUILD_TYPE + + - name: Run failover tests + if: always() + working-directory: ${{ github.workspace }}/build/unit_testing + shell: bash + run: ctest -C $WINDOWS_BUILD_TYPE --output-on-failure + + - name: Check memory leaks + if: always() + working-directory: ${{ github.workspace }}/build/unit_testing/Testing/Temporary + run: | + $is_leaking = Select-String -Path ./LastTest.log -Pattern 'leak' -SimpleMatch + if ($is_leaking) {echo $is_leaking; exit 1;} + + # Upload artifacts + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-failover-sln + path: ${{ github.workspace }}/build/MySQL_Connector_ODBC.sln + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-failover-binaries + path: ${{ github.workspace }}/build/bin/ + - name: Upload build artifacts - Libraries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-failover-libraries + path: ${{ github.workspace }}/build/lib/ + - name: Upload failover test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-failover-results + path: ${{ github.workspace }}/build/unit_testing/Testing/Temporary/LastTest.log + + build-linux: + name: Linux + runs-on: ubuntu-20.04 + env: + CMAKE_GENERATOR: Unix Makefiles + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs & other dependencies + run: sudo apt-get update && sudo apt-get install + build-essential + libgtk-3-dev + libmysqlclient-dev + unixodbc + unixodbc-dev + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$LINUX_BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=true + -DWITH_UNIXODBC=1 + -DENABLE_UNIT_TESTS=TRUE + -DENABLE_INTEGRATION_TESTS=FALSE + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: cmake --build . --config $LINUX_BUILD_TYPE + + # Test driver + - name: Run failover tests on Linux + if: success() + working-directory: ${{ github.workspace }}/build/unit_testing + shell: bash + run: ctest --output-on-failure + + # Upload artifacts + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: linux-failover-binaries + path: ${{ github.workspace }}/build/bin/ + - name: Upload build artifacts - Libraries + if: always() + uses: actions/upload-artifact@v2 + with: + name: linux-failover-libraries + path: ${{ github.workspace }}/build/lib/ + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: linux-failover-results + path: ${{ github.workspace }}/build/unit_testing/Testing/Temporary/LastTest.log + + build-mac: + name: MacOS + runs-on: macos-11 + env: + CMAKE_GENERATOR: Unix Makefiles + MYSQL_DIR: /usr/local/opt/mysql-client + ODBC_DM_INCLUDES: /usr/local/include + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + # Removing some /usr/local/bin files to avoid symlink issues wih brew update + - name: Install MySQL client libs & other dependencies + run: | + rm '/usr/local/bin/2to3' + rm '/usr/local/bin/2to3-3.11' + rm '/usr/local/bin/idle3' + rm '/usr/local/bin/idle3.11' + rm '/usr/local/bin/pydoc3' + rm '/usr/local/bin/pydoc3.11' + rm '/usr/local/bin/python3' + rm '/usr/local/bin/python3-config' + rm '/usr/local/bin/python3.11' + rm '/usr/local/bin/python3.11-config' + + brew update + brew unlink unixodbc + brew install libiodbc mysql-client + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$MAC_BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=true + -DODBC_INCLUDES=$ODBC_DM_INCLUDES + -DENABLE_UNIT_TESTS=TRUE + -DENABLE_INTEGRATION_TESTS=FALSE + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: | + export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix zstd)/lib/ + cmake --build . + + # Test driver + - name: Run driver tests + if: success() + working-directory: ${{ github.workspace }}/build/unit_testing + shell: bash + run: ctest + + - name: Check memory leaks + if: always() + working-directory: ${{ github.workspace }}/build/unit_testing/bin + run: | + leaks -atExit -- ./unit_testing > ../leaks_unit_testing.txt + export is_leaking=$? + if (( $is_leaking != 0 )); then echo $is_leaking; exit 1; else echo "no memory leaks"; fi; + + # Upload artifacts + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-binaries + path: ${{ github.workspace }}/build/bin/ + - name: Upload build artifacts - Libraries + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-libraries + path: ${{ github.workspace }}/build/lib/ + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-failover-results + path: ${{ github.workspace }}/build/unit_testing/Testing/Temporary/LastTest.log + - name: Upload memory leaks check + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-memory-leaks-results + path: ${{ github.workspace }}/build/unit_testing/leaks_unit_testing.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..1946d893 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,204 @@ +name: Community Tests + +on: + push: + branches: + - main + - myodbc + pull_request: + branches: + - '*' + paths-ignore: + - '**/*.md' + - '**/*.jpg' + - '**/README.txt' + - 'docs/**' + - 'ISSUE_TEMPLATE/**' + - '**/remove-old-artifacts.yml' + +env: + BUILD_TYPE: Release + +jobs: + build-windows: + name: Windows + runs-on: windows-2019 + env: + CMAKE_GENERATOR: Visual Studio 16 2019 + MYSQL_DIR: C:/mysql-8.0.30-winx64 + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs + run: | + curl https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.30-winx64.zip -o mysql.zip + unzip -d C:/ mysql.zip + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DMYSQL_SQL="C:/mysql-8.0.30-winx64" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=TRUE + + # Configure test environment + - name: Build Driver and Copy files + shell: bash + working-directory: ${{ github.workspace }}/build + run: | + cmake --build . --config $BUILD_TYPE + cp -r lib/Release/* C:/Windows/System32/ + cp -r bin/Release/* C:/Windows/System32/ + + - name: Add DSN to registry + shell: bash + working-directory: C:/Windows/System32 + run: | + ./myodbc-installer -d -a -n "MySQL ODBC 8.0 Driver" -t "DRIVER=myodbc8a.dll;SETUP=myodbc8S.dll" + ./myodbc-installer -s -a -c2 -n "test" -t "DRIVER=MySQL ODBC 8.0 Driver;SERVER=localhost;DATABASE=test;UID=root;PWD=" + + - name: Start MySQL server for tests + if: success() + shell: bash + run: | + $MYSQL_DIR/bin/mysqld --initialize-insecure --console + $MYSQL_DIR/bin/mysqld --console & + sleep 20 + $MYSQL_DIR/bin/mysql -u root -e "create database test" + + # Test driver + - name: Run community tests + if: success() + working-directory: ${{ github.workspace }}/build/test + shell: bash + run: ctest -C $BUILD_TYPE --output-on-failure + env: + TEST_DSN: test + TEST_UID: root + TEST_DATABASE: test + TEST_DRIVER: MySQL ODBC 8.0 Driver + + # Upload artifacts + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-sln + path: ${{ github.workspace }}/build/MySQL_Connector_ODBC.sln + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-binaries + path: ${{ github.workspace }}/build/bin/ + - name: Upload build artifacts - Libraries + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-community-libraries + path: ${{ github.workspace }}/build/lib/ + - name: Upload community test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: windows-community-results + path: ${{ github.workspace }}/build/test/Testing/Temporary/LastTest.log + + build-mac: + name: MacOS + runs-on: macos-11 + env: + CMAKE_GENERATOR: Unix Makefiles + MYSQL_DIR: /usr/local/opt/mysql-client + ODBC_DM_INCLUDES: /usr/local/include + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + # Removing some /usr/local/bin files to avoid symlink issues wih brew update + - name: Install MySQL client libs & other dependencies + run: | + rm '/usr/local/bin/2to3' + rm '/usr/local/bin/2to3-3.11' + rm '/usr/local/bin/idle3' + rm '/usr/local/bin/idle3.11' + rm '/usr/local/bin/pydoc3' + rm '/usr/local/bin/pydoc3.11' + rm '/usr/local/bin/python3' + rm '/usr/local/bin/python3-config' + rm '/usr/local/bin/python3.11' + rm '/usr/local/bin/python3.11-config' + + brew update + brew unlink unixodbc + brew install libiodbc mysql mysql-client + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=true + -DODBC_INCLUDES=$ODBC_DM_INCLUDES + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: | + export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix zstd)/lib/ + cmake --build . + + # Configure test environment + - name: Start MySQL server for tests + if: success() + working-directory: ${{ github.workspace }}/test/docker + shell: bash + run: | + mysql.server start + mysql -u root -e "create database test" + # Test driver + - name: Run driver tests + if: success() + working-directory: ${{ github.workspace }}/build/test + shell: bash + run: ctest + env: + TEST_DSN: myodbc8a + TEST_UID: root + TEST_PASSWORD: + TEST_DRIVER: ${{ github.workspace }}/build/lib/libmyodbc8a.dylib + ODBCINI: ${{ github.workspace }}/build/test/odbc.ini + ODBCINSTINI: ${{ github.workspace }}/build/test/odbcinst.ini + + # Upload artifacts + - name: Upload build artifacts - Binaries + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-community-binaries + path: ${{ github.workspace }}/build/bin/ + - name: Upload build artifacts - Libraries + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-libraries + path: ${{ github.workspace }}/build/lib/ + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: macos-community-results + path: ${{ github.workspace }}/build/test/Testing/Temporary/LastTest.log diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 00000000..55cb48cb --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,157 @@ +name: Dockerized Performance Tests + +# This workflow should always be triggered manually +on: + workflow_dispatch: + +env: + BUILD_TYPE: Release + +jobs: + build-dockerized-performance-tests: + concurrency: # Cancel previous runs in the same branch + group: environment-${{ github.ref }} + cancel-in-progress: true + name: Dockerized Performance Tests + runs-on: ubuntu-20.04 + env: + CMAKE_GENERATOR: Unix Makefiles + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs & other dependencies + run: sudo apt-get update && sudo apt-get install + build-essential + libgtk-3-dev + libmysqlclient-dev + unixodbc + unixodbc-dev + curl + libcurl4-openssl-dev + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + # Performance tests are disabled by default + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=TRUE + -DENABLE_INTEGRATION_TESTS=TRUE + -DENABLE_PERFORMANCE_TESTS=TRUE + -DENABLE_UNIT_TESTS=FALSE + -DWITH_UNIXODBC=1 + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + + - name: 'Set up JDK 8' + uses: actions/setup-java@v1 + with: + java-version: 8 + + - name: 'Configure AWS Credentials' + uses: aws-actions/configure-aws-credentials@v1 + 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 10800 \ + --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' + working-directory: ${{ github.workspace }}/testframework + run: | + ./gradlew --no-parallel --no-daemon test-failover --info + env: + TEST_DSN: atlas + TEST_USERNAME: ${{ secrets.TEST_USERNAME }} + TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} + TEST_DB_CLUSTER_IDENTIFIER: ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} + 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 }} + DRIVER_PATH: ${{ github.workspace }}/build + + - name: 'Get Github Action IP' + id: ip + uses: haythem/public-ip@v1.2 + + - name: 'Remove Github Action IP' + if: always() + run: | + aws ec2 revoke-security-group-ingress \ + --group-name default \ + --protocol tcp \ + --port 3306 \ + --cidr ${{ steps.ip.outputs.ipv4 }}/32 \ + 2>&1 > /dev/null; + + - name: 'Display and save log' + if: always() + working-directory: ${{ github.workspace }}/build + run: | + echo "Displaying logs" + mkdir -p ./reports/tests + if [[ -f myodbc.log && -s myodbc.log ]]; then + cat myodbc.log + cp myodbc.log ./reports/tests/myodbc.log + fi + if [[ -f failover_performance.xlsx && -s failover_performance.xlsx ]]; then + cp failover_performance.xlsx ./reports/tests/failover_performance.xlsx + fi + if [[ -f efm_performance.xlsx && -s efm_performance.xlsx ]]; then + cp efm_performance.xlsx ./reports/tests/efm_performance.xlsx + fi + if [[ -f efm_detection_performance.xlsx && -s efm_detection_performance.xlsx ]]; then + cp efm_detection_performance.xlsx ./reports/tests/efm_detection_performance.xlsx + fi + + - name: 'Archive log results' + if: always() + uses: actions/upload-artifact@v2 + with: + name: 'performance-test-logs' + path: build/reports/tests/ + retention-days: 3 + + - name: 'Delete Test Clusters if Ungraceful Cancel' + if: cancelled() + run: | + db_instances=($(aws rds describe-db-clusters \ + --db-cluster-identifier ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} \ + --query 'DBClusters[].DBClusterMembers[].DBInstanceIdentifier' \ + --output text \ + | xargs)); + for ((i = 0; i < ${#db_instances[@]}; i++)); \ + do \ + aws rds delete-db-instance \ + --db-instance-identifier ${db_instances[i]} \ + --skip-final-snapshot \ + 2>&1 > /dev/null; \ + done; + aws rds delete-db-cluster \ + --db-cluster-identifier ${{ secrets.TEST_DB_CLUSTER_IDENTIFIER }}-${{ github.run_id }}-${{ github.run_attempt }} \ + --skip-final-snapshot \ + 2>&1 > /dev/null; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6498422a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,178 @@ +name: Release Draft +# This workflow is triggered on creating tags +on: + workflow_dispatch: + push: + tags: + - "*.*.*" + +env: + BUILD_TYPE: Release + +jobs: + build-mac: + name: macOS + runs-on: macos-11 + env: + CMAKE_GENERATOR: Unix Makefiles + MYSQL_DIR: /usr/local/opt/mysql-client + ODBC_DM_INCLUDES: /usr/local/include + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + # Removing some /usr/local/bin files to avoid symlink issues wih brew update + - name: Install MySQL client libs & other dependencies + run: | + rm '/usr/local/bin/2to3' + rm '/usr/local/bin/2to3-3.11' + rm '/usr/local/bin/idle3' + rm '/usr/local/bin/idle3.11' + rm '/usr/local/bin/pydoc3' + rm '/usr/local/bin/pydoc3.11' + rm '/usr/local/bin/python3' + rm '/usr/local/bin/python3-config' + rm '/usr/local/bin/python3.11' + rm '/usr/local/bin/python3.11-config' + + brew update + brew unlink unixodbc + brew install libiodbc mysql-client + + - name: Create build environment + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=true + -DODBC_INCLUDES=$ODBC_DM_INCLUDES + -DCONNECTOR_PLATFORM=macos + + - name: Build driver + working-directory: ${{ github.workspace }}/build + run: | + export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix zstd)/lib/ + cmake --build . + + - name: Build installer + working-directory: ${{ github.workspace }}/build + if: success() + run: cpack . + + - name: Upload macOS installer as artifact + if: success() + uses: actions/upload-artifact@v3 + with: + name: installers + path: ${{ github.workspace }}/build/mysql-connector-odbc-*.pkg + if-no-files-found: error + + build-linux: + name: Linux + runs-on: ubuntu-20.04 + env: + CMAKE_GENERATOR: Unix Makefiles + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install build dependencies + run: sudo apt-get update && sudo apt-get install + build-essential + libgtk-3-dev + unixodbc + unixodbc-dev + + - name: Install MySQL client libs and include files + run: | + curl -L https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.31-linux-glibc2.12-x86_64.tar.xz -o mysql.tar.gz + tar xf mysql.tar.gz + + - name: Create build environment + shell: bash + run: cmake -E make_directory ${{ github.workspace }}/build + + - name: Configure CMake + shell: bash + run: cmake -S . -B build + -G "$CMAKE_GENERATOR" + -DCMAKE_BUILD_TYPE=$BUILD_TYPE + -DMYSQLCLIENT_STATIC_LINKING=true + -DWITH_UNIXODBC=1 + -DCONNECTOR_PLATFORM=linux + -DMYSQL_DIR=./mysql-8.0.31-linux-glibc2.12-x86_64/ + + # Build driver + - name: Build driver + working-directory: ${{ github.workspace }}/build + shell: bash + run: cmake --build . --config $BUILD_TYPE + + - name: Build installer + working-directory: ${{ github.workspace }}/build + if: success() + run: cpack . + + - name: Upload Linux installer as artifact + if: success() + uses: actions/upload-artifact@v3 + with: + name: installers + path: ${{ github.workspace }}/build/mysql-connector-odbc-*.tar.gz + if-no-files-found: error + + build-windows: + name: Windows + runs-on: windows-2019 + env: + CMAKE_GENERATOR: Visual Studio 16 2019 + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + # Configure build environment/dependencies + - name: Install MySQL client libs + run: | + curl -L https://cdn.mysql.com/archives/mysql-8.0/mysql-8.0.30-winx64.zip -o mysql.zip + unzip -d C:/ mysql.zip + + - name: Setup nmake + uses: ilammy/msvc-dev-cmd@v1 + + - name: Run build installer script + run: | + .\build_installer.ps1 x64 $BUILD_TYPE C:/mysql-8.0.30-winx64 + + - name: Upload Windows installer as artifact + if: success() + uses: actions/upload-artifact@v3 + with: + name: installers + path: ${{ github.workspace }}/wix/*.msi + if-no-files-found: error + + draft-release: + name: Create Draft Release + runs-on: ubuntu-20.04 + needs: [build-mac, build-linux, build-windows] + steps: + - name: Download all installers + uses: actions/download-artifact@v3 + with: + name: installers + + # Get tag version for release + - name: Set Version Env Variable + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Upload to Draft Release + uses: ncipollo/release-action@v1 + with: + draft: true + name: "MySQL Connector/ODBC Driver - v${{ env.RELEASE_VERSION }}" + artifacts: "./*.pkg, ./*.tar.gz, ./*.msi" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/remove-old-artifacts.yml b/.github/workflows/remove-old-artifacts.yml new file mode 100644 index 00000000..a3cea06d --- /dev/null +++ b/.github/workflows/remove-old-artifacts.yml @@ -0,0 +1,18 @@ +name: Remove Old Artifacts + +on: + schedule: + # Every day at 1am + - cron: '0 1 * * *' + +jobs: + remove-old-artifacts: + runs-on: ubuntu-20.04 + timeout-minutes: 10 + + steps: + - name: Remove Old Artifacts + uses: c-hive/gha-remove-artifacts@v1 + with: + age: '1 week' + skip-tags: true diff --git a/.gitignore b/.gitignore index 4e3f2579..87c9cbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,47 @@ -driver/drivera.def +# Build artifacts +build/ +bin/ +lib/ +out/ +x64/ +x86/ +**/*.dir +*.pkg + +# Test artifacts +test/Testing/** +# Ignore all test binaries, and keep everything else +test/* +!test/docker +!test/CMakeLists.txt +!test/*.c +!test/*.cc +!test/*.h +!test/*.in +!test/unit.pl +unit_testing/build/** +unit_testing/bin/** +integration/bin/** + +# Test dependencies +_deps/** +*/_deps/** + +# CMake generated files +CPack*.cmake +**/CMakeFiles +**/cmake_install.cmake +**/CMakeCache.txt +**/Makefile +cmake/sql*.c +*.cmake +mysql_strings/uca900_ja_tbls.cc +mysql_strings/uca900_zh_tbls.cc +getmysqlversion.c +_CPack_Packages/ +install_manifest*.txt + +driver/drivera.def driver/drivera.rc driver/driverw.def driver/driverw.rc @@ -9,3 +52,44 @@ INFO_SRC Install.bat myodbc3.spec Uninstall.bat + +# Visual Studio files +/.vs +/.vscode +*.vcxproj* +*.sln +**/*.aps + +# Java build files +testframework/bin/ +testframework/build/ +testframework/buildtest/ +testframework/dist/ +testframework/nbproject/ +testframework/patches/ +testframework/src/test/resources/*.ini +*~ +*.lock +*.swp +*.DS_Store +*.diff +*.dot +*.dot.png +*.class +*.iml +*.project.elrade +*.attach_pid* +*.gradle +*.idea +testframework/hs_err_pid*.log +*.sh +*.patch + +# Wix files +*.wixobj +*.wixpdb +/wix/mysql-odbc-*.xml +/wix/*.msi +/wix/myodbc_version.xml +/wix/x64 +/wix/x86 diff --git a/CMakeLists.txt b/CMakeLists.txt index d76e5ef2..692969b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,5 @@ +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2007, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify @@ -83,6 +85,22 @@ endif() #----------------------------------------------------- +include(FetchContent) +FetchContent_Declare( + ctpl + URL https://github.com/vit-vit/CTPL/archive/refs/tags/ctpl_v.0.0.2.zip +) + +FetchContent_MakeAvailable(ctpl) + +#----------------------------------------------------- + +IF(ENABLE_UNIT_TESTS) + ADD_DEFINITIONS(-DUNIT_TEST_BUILD) +ENDIF(ENABLE_UNIT_TESTS) + +#----------------------------------------------------- + FIND_PACKAGE(Threads) INCLUDE(version.cmake) @@ -285,7 +303,6 @@ ENDIF(MYSQL8) CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/VersionInfo.h.cmake ${CMAKE_SOURCE_DIR}/VersionInfo.h @ONLY) -CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/scripts/macosx/postflight.in ${CMAKE_SOURCE_DIR}/scripts/macosx/postflight @ONLY) IF(WIN32) CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/Install.bat.cmake ${CMAKE_SOURCE_DIR}/Install.bat @ONLY) CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/Uninstall.bat.cmake ${CMAKE_SOURCE_DIR}/Uninstall.bat @ONLY) @@ -446,9 +463,13 @@ IF(WIN32) # Add some additional help for debug builds IF(CMAKE_BUILD_TYPE STREQUAL "Debug") STRING(REPLACE "/Zi" "/ZI" NEW_FLAGS "${NEW_FLAGS}") - SET(NEW_FLAGS "${NEW_FLAGS} /RTC1 /RTCc") + SET(NEW_FLAGS "${NEW_FLAGS} /RTC1") ENDIF(CMAKE_BUILD_TYPE STREQUAL "Debug") + IF(CMAKE_BUILD_TYPE STREQUAL "RELWITHDEBINFO") + SET(NEW_FLAGS "${NEW_FLAGS} /Od") + ENDIF(CMAKE_BUILD_TYPE STREQUAL "RELWITHDEBINFO") + # *FORCE* to override whats already placed into the cache SET(CMAKE_${TYPE}_FLAGS${CFG} "${NEW_FLAGS}" CACHE STRING "CMAKE_${TYPE}_FLAGS${CFG} (overwritten for odbc)" FORCE) @@ -508,23 +529,57 @@ setup_ssl_libs() INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}) +INCLUDE_DIRECTORIES(${ctpl_SOURCE_DIR}) + ADD_SUBDIRECTORY(util) ADD_SUBDIRECTORY(driver) IF(NOT DISABLE_GUI) - ADD_SUBDIRECTORY(setupgui) + ADD_SUBDIRECTORY(setupgui) ENDIF(NOT DISABLE_GUI) ADD_SUBDIRECTORY(dltest) ADD_SUBDIRECTORY(installer) ADD_SUBDIRECTORY(test) +IF(ENABLE_INTEGRATION_TESTS OR ENABLE_PERFORMANCE_TESTS) + ADD_SUBDIRECTORY(integration) +ENDIF() + +IF(ENABLE_UNIT_TESTS) + ADD_SUBDIRECTORY(unit_testing) +ENDIF() + # For dynamic linking use the built-in sys and strings IF(NOT MYSQLCLIENT_STATIC_LINKING) ADD_SUBDIRECTORY(mysql_sys) ADD_SUBDIRECTORY(mysql_strings) ENDIF() +# Set values to help generate odbc.ini and odbcinst.ini files for Dockerized tests +IF(ENABLE_UNIT_TESTS OR ENABLE_INTEGRATION_TESTS) + SET(ODBC_PATH ${CMAKE_SOURCE_DIR}/testframework/src/test/resources/) + SET(TEST_DRIVER "MyODBCa") + SET(DRIVER_PATH "/app/lib/libmyodbc8a.so") + SET(DRIVER_DESCRIPTION "ANSI Driver for connecting to MySQL database server") + SET(DESCRIPTION "MySQL ODBC ${CONNECTOR_MAJOR}.${CONNECTOR_MINOR} ANSI Driver for Dockerized tests") + + IF(ENABLE_UNIT_TESTS) + SET(TEST_DSN "myodbc8a") + SET(TEST_DATABASE "test") + SET(TEST_SERVER "mysql-instance") + SET(TEST_UID "root") + ENDIF() + + IF(ENABLE_INTEGRATION_TESTS) + SET(TEST_DSN "atlas") + SET(THREADING "0") + ENDIF() + + CONFIGURE_FILE(${ODBC_PATH}/odbcinst.ini.in ${ODBC_PATH}/odbcinst.ini @ONLY) + CONFIGURE_FILE(${ODBC_PATH}/odbc.ini.in ${ODBC_PATH}/odbc.ini @ONLY) +ENDIF() + ############################################################################## # # Packaging @@ -555,21 +610,31 @@ SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Connector/ODBC ${CONNECTOR_BASE_VERSION}, SET(CPACK_PACKAGE_NAME "mysql-connector-odbc${EXTRA_NAME_SUFFIX}") SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt") -if(EXISTS "${CMAKE_SOURCE_DIR}/README.txt") +IF(EXISTS "${CMAKE_SOURCE_DIR}/README.txt") SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.txt") -else() +ELSE() SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md") -endif() +ENDIF() SET(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CONNECTOR_VERSION}-src") SET(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}-${CONNECTOR_VERSION}${EXTRA_NAME_SUFFIX2}${PACKAGE_DRIVER_TYPE_SUFFIX}-${CONNECTOR_PLATFORM}") IF(WIN32) SET(CPACK_GENERATOR "ZIP") SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-noinstall-${CONNECTOR_VERSION}${EXTRA_NAME_SUFFIX2}${PACKAGE_DRIVER_TYPE_SUFFIX}-${CONNECTOR_PLATFORM}") -ELSE(WIN32) +ELSEIF(APPLE) + SET(CPACK_GENERATOR "productbuild") + SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_INSTALL_DIRECTORY}") + SET(CPACK_PACKAGING_INSTALL_PREFIX "/usr/local/${CPACK_PACKAGE_FILE_NAME}") + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/scripts/macosx/Welcome.html.in ${CMAKE_SOURCE_DIR}/scripts/macosx/Welcome.html @ONLY) + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/scripts/macosx/ReadMe.html.in ${CMAKE_SOURCE_DIR}/scripts/macosx/ReadMe.html @ONLY) + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/scripts/macosx/postflight.in ${CMAKE_SOURCE_DIR}/scripts/macosx/postflight @ONLY) + SET(CPACK_RESOURCE_FILE_WELCOME "${CMAKE_CURRENT_SOURCE_DIR}/scripts/macosx/Welcome.html") + SET(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/scripts/macosx/ReadMe.html") + SET(CPACK_POSTFLIGHT_UNSPECIFIED_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/scripts/macosx/postflight") +ELSE() SET(CPACK_GENERATOR "TGZ") SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_INSTALL_DIRECTORY}") -ENDIF(WIN32) +ENDIF() SET(CPACK_SOURCE_IGNORE_FILES \\\\.bzr/ @@ -768,6 +833,12 @@ function(bundle_lib lib) endfunction(bundle_lib) +macro(copy_lib_for_wix lib) + + get_filename_component(lib_name ${lib} NAME) + configure_file(${lib} ${CMAKE_SOURCE_DIR}/wix/x64/${lib_name} COPYONLY) + +endmacro(copy_lib_for_wix) # Bundle libraries listed in a list variable ${to_bundle}. # Libraries that were found and bundled are removed from ${to_bundle} list. @@ -822,6 +893,7 @@ macro(bundle_libs to_bundle ignored) #message(STATUS "removing from list: ${name}") list(REMOVE_ITEM ${to_bundle} ${name}) bundle_lib(${lib}) + copy_lib_for_wix(${lib}) set(found ${name}) break() endif() @@ -893,6 +965,8 @@ macro(bundle_plugins ignored) DESTINATION "${LIB_SUBDIR}/plugin" COMPONENT ODBCDll ) + + configure_file(${lib} ${CMAKE_SOURCE_DIR}/wix/x64/plugin/${lib_name}.dll COPYONLY) # See if libsasl is bundled to also bundle sasl plugins if("${DEPS_${plugin}}" MATCHES "sasl") diff --git a/CreateBinaryMsi.bat b/CreateBinaryMsi.bat index a33797b7..126c5645 100644 --- a/CreateBinaryMsi.bat +++ b/CreateBinaryMsi.bat @@ -1,5 +1,7 @@ @ECHO OFF +REM Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +REM REM Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. REM REM This program is free software; you can redistribute it and/or modify diff --git a/GoogleTest.LICENSE b/GoogleTest.LICENSE new file mode 100644 index 00000000..1941a11f --- /dev/null +++ b/GoogleTest.LICENSE @@ -0,0 +1,28 @@ +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Install.bat.cmake b/Install.bat.cmake index a7609762..6e8ce614 100644 --- a/Install.bat.cmake +++ b/Install.bat.cmake @@ -1,4 +1,6 @@ @ECHO OFF +REM Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +REM REM Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. REM REM This program is free software; you can redistribute it and/or modify diff --git a/MYODBC_MYSQL.h b/MYODBC_MYSQL.h index 75fe3c53..7d2a9121 100644 --- a/MYODBC_MYSQL.h +++ b/MYODBC_MYSQL.h @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -94,7 +96,7 @@ extern "C" #define my_sys_init my_init -#define x_free(A) { void *tmp= (A); if (tmp) my_free((char *) tmp); } +#define x_free(A) { void *tmpBuf= (A); if (tmpBuf) my_free((char *) tmpBuf); } #define myodbc_malloc(A,B) my_malloc(PSI_NOT_INSTRUMENTED,A,B) #define myodbc_realloc(A,B,C) my_realloc(PSI_NOT_INSTRUMENTED,A,B,C) diff --git a/MYODBC_ODBC.h b/MYODBC_ODBC.h index b5ae49fa..ea23adce 100644 --- a/MYODBC_ODBC.h +++ b/MYODBC_ODBC.h @@ -1,30 +1,32 @@ -// Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License, version 2.0, as -// published by the Free Software Foundation. -// -// This program is also distributed with certain software (including -// but not limited to OpenSSL) that is licensed under separate terms, -// as designated in a particular file or component or in included license -// documentation. The authors of MySQL hereby grant you an -// additional permission to link the program and your derivative works -// with the separately licensed software that they have included with -// MySQL. -// -// Without limiting anything contained in the foregoing, this file, -// which is part of MySQL Connector/ODBC, is also subject to the -// Universal FOSS Exception, version 1.0, a copy of which can be found at -// http://oss.oracle.com/licenses/universal-foss-exception. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU General Public License, version 2.0, for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0, as +// published by the Free Software Foundation. +// +// This program is also distributed with certain software (including +// but not limited to OpenSSL) that is licensed under separate terms, +// as designated in a particular file or component or in included license +// documentation. The authors of MySQL hereby grant you an +// additional permission to link the program and your derivative works +// with the separately licensed software that they have included with +// MySQL. +// +// Without limiting anything contained in the foregoing, this file, +// which is part of MySQL Connector/ODBC, is also subject to the +// Universal FOSS Exception, version 1.0, a copy of which can be found at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #ifndef MYODBC_ODBC_H #define MYODBC_ODBC_H @@ -58,9 +60,6 @@ # endif #else # include -# ifndef RC_INVOKED -# pragma pack(1) -# endif # include # include diff --git a/README.md b/README.md index 45b4c9dc..64c60ed0 100644 --- a/README.md +++ b/README.md @@ -6,50 +6,777 @@ For detailed information please visit the official [MySQL Connector/ODBC documen Source packages are available from our [github releases page](https://github.com/mysql/mysql-connector-odbc/releases). -## Licensing +**The MySQL Connector/ODBC Driver** allows an application to take advantage of the features of clustered MySQL databases. -Please refer to files README and LICENSE, available in this repository, and [Legal Notices in documentation](https://dev.mysql.com/doc/connector-cpp/8.0/en/preface.html) for further details. +## Table of Contents +- [MySQL Connector/ODBC Driver](#mysql-connectorodbc-driver) + - [Table of Contents](#table-of-contents) + - [What is Failover?](#what-is-failover) + - [Benefits of the MySQL Connector/ODBC Driver](#benefits-of-the-mysql-connectorodbc-driver) + - [Getting Started](#getting-started) + - [Installing the MySQL Connector/ODBC Driver](#installing-the-mysql-connectorodbc-driver) + - [Windows](#windows) + - [MacOS](#macos) + - [Linux](#linux) + - [Configuring the Driver and DSN Entries](#configuring-the-driver-and-dsn-entries) + - [odbc.ini](#odbcini) + - [odbcinst.ini](#odbcinstini) + - [Failover Process](#failover-process) + - [Connection Strings and Configuring the Driver](#connection-strings-and-configuring-the-driver) + - [Failover Specific Options](#failover-specific-options) + - [Driver Behaviour During Failover For Different Connection URLs](#driver-behaviour-during-failover-for-different-connection-urls) + - [Host Pattern](#host-pattern) + - [Advanced Failover Configuration](#advanced-failover-configuration) + - [Failover Time Profiles](#failover-time-profiles) + - [Example of the configuration for a normal failover time profile:](#example-of-the-configuration-for-a-normal-failover-time-profile) + - [Example of the configuration for an aggressive failover time profile:](#example-of-the-configuration-for-an-aggressive-failover-time-profile) + - [Writer Cluster Endpoints After Failover](#writer-cluster-endpoints-after-failover) + - [2-Node Clusters](#2-node-clusters) + - [Node Availability](#node-availability) + - [Enhanced Failure Monitoring](#enhanced-failure-monitoring) + - [The Benefits Enhanced Failure Monitoring](#the-benefits-enhanced-failure-monitoring) + - [Simple Network Timeout](#simple-network-timeout) + - [Enabling Host Monitoring](#enabling-host-monitoring) + - [Enhanced Failure Monitoring Parameters](#enhanced-failure-monitoring-parameters) + - [Failover Exception Codes](#failover-exception-codes) + - [08S01 - Communication Link Failure](#08s01---communication-link-failure) + - [08S02 - Communication Link Changed](#08s02---communication-link-changed) + - [Sample Code](#sample-code) + - [08007 - Connection Failure During Transaction](#08007---connection-failure-during-transaction) + - [Sample Code](#sample-code-1) + - [Building the MySQL Connector/ODBC Driver](#building-the-mysql-connectorodbc-driver) + - [Windows](#windows-1) + - [MacOS](#macos-1) + - [Troubleshoot](#troubleshoot) + - [Linux](#linux-1) + - [Testing the MySQL Connector/ODBC Driver](#testing-the-mysql-connectorodbc-driver) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) + - [Integration Tests Against A MySQL Server](#integration-tests-against-a-mysql-server) + - [Prerequisites](#prerequisites) + - [Steps](#steps) + - [Failover-specific Integration Tests](#failover-specific-integration-tests) + - [Prerequisites](#prerequisites-1) + - [Steps](#steps-1) + - [Getting Help and Opening Issues](#getting-help-and-opening-issues) + - [Logging](#logging) + - [Enabling Logs On Windows](#enabling-logs-on-windows) + - [Example](#example) + - [Enabling Logs On MacOS and Linux](#enabling-logs-on-macos-and-linux) + - [License](#license) -## Download & Install +## What is Failover? -MySQL Connector/ODBC can be installed from pre-compiled packages that can be downloaded from the [MySQL downloads page](https://dev.mysql.com/downloads/connector/odbc/). -The process of installing of Connector/ODBC from a binary distribution is described in [MySQL online manuals](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-installation.html) +In an Amazon Aurora database (DB) cluster, failover is a mechanism by which Aurora automatically repairs the DB cluster status when a primary DB instance becomes unavailable. It achieves this goal by electing an Aurora Replica to become the new primary DB instance, so that the DB cluster can provide maximum availability to a primary read-write DB instance. The MySQL Connector/ODBC Driver is designed to coordinate with this behavior in order to provide minimal downtime in the event of a DB instance failure. -### Building from sources +## Benefits of the MySQL Connector/ODBC Driver -MySQL Connector/ODBC can be installed from the source. Please select the relevant platform in [MySQL online manuals](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-installation.html) +Although Aurora is able to provide maximum availability through the use of failover, existing client drivers do not currently support this functionality. This is partially due to the time required for the DNS of the new primary DB instance to be fully resolved in order to properly direct the connection. The MySQL Connector/ODBC Driver fully utilizes failover behavior by maintaining a cache of the Aurora cluster topology and each DB instance's role (Aurora Replica or primary DB instance). This topology is provided via a direct query to the Aurora database, essentially providing a shortcut to bypass the delays caused by DNS resolution. With this knowledge, the MySQL Connector/ODBC Driver can more closely monitor the Aurora DB cluster status so that a connection to the new primary DB instance can be established as fast as possible. Additionally, the MySQL Connector/ODBC Driver can be used to interact with Aurora MySQL, RDS MySQL, and commercial/open-source MySQL databases. -### GitHub Repository +## Getting Started -This repository contains the MySQL Connector/ODBC source code as per latest released version. You should expect to see the same contents here and within the latest released Connector/ODBC package. +### Installing the MySQL Connector/ODBC Driver -## Usage Scenarios +#### Windows -The MySQL Connector/ODBC can be used in a variety of programming languages and applications. -The most popular of them are: +Download the `.msi` Windows installer for your system; execute the installer and follow the onscreen instructions. The default target installation location for the driver files is `C:\Program Files\MySQL\Connector ODBC 8.0`. An ANSI driver and a Unicode driver will be installed, named respectively `MySQL ODBC ANSI Driver` and `MySQL ODBC 8.0 Unicode Driver`. To uninstall the ODBC driver, open the same installer file, select the option to uninstall the driver and follow the onscreen instructions to successfully uninstall the driver. -* C and C++ programming using ODBC API -* C++ programming using ADODB objects -* Visual Basic programming using ADODB objects -* Java through JDBC/ODBC bridge -* .NET platform with ADO.NET/ODBC bridge -* PHP, Perl, Python, Ruby, Erlang. -* Office applications through linked tables and Visual Basic integration -* Multitude of other applications supporting ODBC +#### MacOS +In order to use the MySQL Connector/ODBC Driver, [iODBC Driver Manager](http://www.iodbc.org/dataspace/doc/iodbc/wiki/iodbcWiki/Downloads) must be installed. `iODBC Driver Manager` contains the required libraries to install, configure the driver and DSN configurations. -## Documentation +Download the `.pkg` installer; run the installer and follow the onscreen instructions. The default target installation location for the driver folder is `/usr/local/`. Note that for a MacOS system, additional steps are required to configure the driver and Data Source Name (DSN) entries before you can use the driver(s). Initially, the installer will register two driver entries with two corresponding DSN entries. For information about [how to configure the driver and DSN settings](#configuring-the-driver-and-dsn-entries), review the configuration sample. There is no uninstaller at this time, but all the driver files can be removed by deleting the target installation directory. -* [MySQL](http://www.mysql.com/) -* [Connector ODBC Developer Guide](https://dev.mysql.com/doc/connector-odbc/en/) -* [ODBC API Reference MSDN](https://msdn.microsoft.com/en-us/ie/ms714562(v=vs.94)) +#### Linux -## Questions/Bug Reports +In order to use the MySQL Connector/ODBC Driver, [unixODBC](http://www.unixodbc.org/) must be installed. -* [Discussion Forum](https://forums.mysql.com/list.php?37) -* [Slack](https://mysqlcommunity.slack.com) -* [Bugs](https://bugs.mysql.com) +For **Ubuntu 64 bit**: -## Contributing +```bash +sudo apt update +sudo apt install unixodbc +``` -Please see our [guidelines](CONTRIBUTING.md) for contributing to the driver. +For **Amazon Linux 2 64 bit**: + +```bash +sudo yum update +sudo yum install unixODBC +``` + +Once `unixODBC` is installed, download the `.tar.gz` file, and extract the contents to your desired location. For a Linux system, additional steps are required to configure the driver and Data Source Name (DSN) entries before the driver(s) can be used. For more information, see [Configuring the Driver and DSN settings](#configuring-the-driver-and-dsn-entries). There is no uninst + +#### Configuring the Driver and DSN Entries + +To configure the driver on Windows, use the `ODBC Data Source Administrator` tool to add or configure a DSN for either the `MySQL ODBC ANSI Driver` or `MySQL ODBC 8.0 Unicode Driver`. With this DSN you can specify the options for the desired connection. Additional configuration properties are available by clicking the `Details >>` button. + +To use the driver on MacOS or Linux systems, you need to create two files (`odbc.ini` and `odbcinst.ini`), that will contain the configuration for the driver and the Data Source Name (DSN). + +You can modify the files manually, or through tools with a GUI such as `iODBC Administrator` (available for MacOS). In the following sections, we show samples of `odbc.ini` and `odbcinst.ini` files that describe how an ANSI driver could be set up for a MacOS system. In a MacOS system, the `odbc.ini` and `odbcinst.ini` files are typically located in the `/Library/ODBC/` directory. + +For a Linux system, the files would be similar, but the driver file would have the `.so` extension instead of the `.dylib` extension shown in the sample. On a Linux system, the `odbc.ini` and `odbcinst.ini` files are typically located in the `/etc` directory. + +##### odbc.ini + +```bash +[ODBC Data Sources] +myodbcw = ODBC Unicode Driver for MySQL +myodbca = ODBC ANSI Driver for MySQL + +[myodbcw] +Driver = ODBC Unicode Driver for MySQL +SERVER = localhost +NO_SCHEMA = 1 +TOPOLOGY_REFRESH_RATE = 30000 +FAILOVER_TIMEOUT = 60000 +FAILOVER_TOPOLOGY_REFRESH_RATE = 5000 +FAILOVER_WRITER_RECONNECT_INTERVAL = 5000 +FAILOVER_READER_CONNECT_TIMEOUT = 30000 +CONNECT_TIMEOUT = 30 +NETWORK_TIMEOUT = 30 + +[myodbca] +Driver = ODBC ANSI Driver for MySQL +SERVER = localhost +NO_SCHEMA = 1 +TOPOLOGY_RR = 30000 +FAILOVER_TIMEOUT = 60000 +FAILOVER_TOPOLOGY_REFRESH_RATE = 5000 +FAILOVER_WRITER_RECONNECT_INTERVAL = 5000 +FAILOVER_READER_CONNECT_TIMEOUT = 30000 +CONNECT_TIMEOUT = 30 +NETWORK_TIMEOUT = 30 +``` + +##### odbcinst.ini + +```bash +[ODBC Drivers] +ODBC Unicode Driver for MySQL = Installed +ODBC ANSI Driver for MySQL = Installed + +[ODBC Unicode Driver for MySQL] +Driver = /usr/local/MySQL_ODBC_Connector/lib/myodbc8w.dylib + +[ODBC ANSI Driver for MySQL] +Driver = /usr/local/MySQL_ODBC_Connector/lib/myodbc8a.dylib +``` + +## Failover Process + +In an Amazon Aurora database (DB) cluster, failover is a mechanism by which Aurora automatically repairs the DB cluster status when a primary DB instance becomes unavailable. It achieves this goal by electing an Aurora Replica to become the new primary DB instance, so that the DB cluster can provide maximum availability to a primary read-write DB instance. The MySQL Connector/ODBC Driver uses the Failover Plugin to coordinate with this behavior in order to provide minimal downtime in the event of a DB instance failure. + +![failover_diagram](docs/images/failover_diagram.png) + +The figure above provides a simplified overview of how the MySQL Connector/ODBC Driver handles an Aurora failover encounter. Starting at the top of the diagram, an application using the driver sends a request to get a logical connection to an Aurora database. + +In this example, the application requests a connection using the Aurora DB cluster endpoint and is returned a logical connection that is physically connected to the primary DB instance in the DB cluster, DB instance C. By design, details about which specific DB instance the physical connection is connected to have been abstracted away. + +Over the course of the application's lifetime, it executes various statements against the logical connection. If DB instance C is stable and active, these statements succeed and the application continues as normal. If DB instance C experiences a failure, Aurora will initiate failover to promote a new primary DB instance. At the same time, the MySQL Connector/ODBC Driver will intercept the related communication exception and kick off its own internal failover process. + +If the primary DB instance has failed, the driver will use its internal topology cache to temporarily connect to an active Aurora Replica. This Aurora Replica will be periodically queried for the DB cluster topology until the new primary DB instance is identified (DB instance A or B in this case). + +At this point, the driver will connect to the new primary DB instance and return control to the application to allow the user to reconfigure the session state as needed. Although the DNS endpoint for the DB cluster might not yet resolve to the new primary DB instance, the driver has already discovered this new DB instance during its failover process, and will be directly connected to it when the application continues executing statements. In this way the driver provides a faster way to reconnect to a newly promoted DB instance, thus increasing the availability of the DB cluster. + +### Connection Strings and Configuring the Driver + +To set up a connection, the driver uses an ODBC connection string. An ODBC connection string specifies a set of semicolon-delimited connection options. Typically, a connection string will either: + +1. specify a Data Source Name containing a preconfigured set of options (`DSN=xxx;`) or +2. configure options explicitly (`SERVER=xxx;PORT=xxx;...;`). This option will override values set inside the DSN. + +### Failover Specific Options + +In addition to the parameters that you can configure for the [MySQL Connector/ODBC driver](https://dev.mysql.com/doc/connector-odbc/en/connector-odbc-configuration-connection-parameters.html), you can configure the following parameters in a DSN or connection string to specify failover behaviour. If the values for these options are not specified, the default values will be used. If you are dealing with the Windows DSN UI, click `Details >>` and navigate to the `Cluster Failover` tab to find the equivalent parameters. + +| Option | Description | Type | Required | Default | +| ---------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------|------------------------------------------| +| `ENABLE_CLUSTER_FAILOVER` | Set to `1` to enable the fast failover behaviour offered by the MySQL Connector/ODBC Driver. | bool | No | `1` | +| `ALLOW_READER_CONNECTIONS` | Set to `1` to allow connections to reader instances during the failover process. | bool | No | `0` | +| `GATHER_PERF_METRICS` | Set to `1` to record failover-associated metrics. | bool | No | `0` | +| `GATHER_PERF_METRICS_PER_INSTANCE` | Set to `1` to gather additional performance metrics per instance as well as cluster. | bool | No | `0` | +| `HOST_PATTERN` | This parameter is not required unless connecting to an AWS RDS cluster via an IP address or custom domain URL. In those cases, this parameter specifies the cluster instance DNS pattern that will be used to build a complete instance endpoint. A "?" character in this pattern should be used as a placeholder for the DB instance identifiers of the instances in the cluster.

Example: `?.my-domain.com`, `any-subdomain.?.my-domain.com:9999`

Usecase Example: If your cluster instance endpoint follows this pattern:`instanceIdentifier1.customHost`, `instanceIdentifier2.customHost`, etc. and you want your initial connection to be to `customHost:1234`, then your connection string should look like this: `SERVER=customHost;PORT=1234;DATABASE=test;HOST_PATTERN=?.customHost`

If the provided connection string is not an IP address or custom domain, the driver will automatically acquire the cluster instance host pattern from the customer-provided connection string. | char\* | If connecting using an IP address or custom domain URL: Yes

Otherwise: No

See [Host Pattern](#host-pattern) for more details. | `NONE` | +| `CLUSTER_ID` | A unique identifier for the cluster. Connections with the same cluster ID share a cluster topology cache. This connection parameter is not required and thus should only be set if desired. | char\* | No | Either the cluster ID or the instance ID, depending on whether the provided connection string is a cluster or instance URL. | +| `TOPOLOGY_REFRESH_RATE` | Cluster topology refresh rate in milliseconds. The cached topology for the cluster will be invalidated after the specified time, after which it will be updated during the next interaction with the connection. | int | No | `30000` | +| `FAILOVER_TIMEOUT` | Maximum allowed time in milliseconds to attempt reconnecting to a new writer or reader instance after a cluster failover is initiated. | int | No | `60000` | +| `FAILOVER_TOPOLOGY_REFRESH_RATE` | Cluster topology refresh rate in milliseconds during a writer failover process. During the writer failover process, cluster topology may be refreshed at a faster pace than normal to speed up discovery of the newly promoted writer. | int | No | `5000` | +| `FAILOVER_WRITER_RECONNECT_INTERVAL` | Interval of time in milliseconds to wait between attempts to reconnect to a failed writer during a writer failover process. | int | No | `5000` | +| `FAILOVER_READER_CONNECT_TIMEOUT` | Maximum allowed time in milliseconds to attempt a connection to a reader instance during a reader failover process. | int | No | `30000` | +| `CONNECT_TIMEOUT` | Timeout (in seconds) for socket connect, with 0 being no timeout. | int | No | `30` | +| `NETWORK_TIMEOUT` | Timeout (in seconds) on network socket operations, with 0 being no timeout. | int | No | `30` | + +### Driver Behaviour During Failover For Different Connection URLs + +![failover_behaviour](docs/images/failover_behaviour.jpg) + +### Host Pattern + +When connecting to Aurora clusters, this parameter is required when the connection string does not provide enough information about the database cluster domain name. If the Aurora cluster endpoint is used directly, the driver will recognize the standard Aurora domain name and can re-build a proper Aurora instance name when needed. In cases where the connection string uses an IP address, a custom domain name or localhost, the driver won't know how to build a proper domain name for a database instance endpoint. For example, if a custom domain was being used and the cluster instance endpoints followed a pattern of `instanceIdentifier1.customHost`, `instanceIdentifier2.customHost`, etc, the driver would need to know how to construct the instance endpoints using the specified custom domain. Because there isn't enough information from the custom domain alone to create the instance endpoints, the `HOST_PATTERN` should be set to `?.customHost`, making the connection string `SERVER=customHost;PORT=1234;DATABASE=test;HOST_PATTERN=?.customHost`. Refer to [Driver Behaviour During Failover For Different Connection URLs](#driver-behaviour-during-failover-for-different-connection-urls) for more examples. + +## Advanced Failover Configuration + +### Failover Time Profiles + +A failover time profile refers to a specific combination of failover parameters that determine the time in which failover should be completed and define the aggressiveness of failover. Some failover parameters include `FAILOTVER_TIMEOUT` and `FAILOVER_READER_CONNECT_TIMEOUT`. Failover should be completed within 5 minutes by default. If the connection is not re-established during this time, then the failover process times out and fails. Users can configure the failover parameters to adjust the aggressiveness of the failover and fulfill the needs of their specific application. For example, a user could take a more aggressive approach and shorten the time limit on failover to promote a fail-fast approach for an application that does not tolerate database outages. Examples of normal and aggressive failover time profiles are shown below. +

+**:warning:Note**: Aggressive failover does come with its side effects. Since the time limit on failover is shorter, it becomes more likely that a problem is caused not by a failure, but rather because of a timeout. +

+ +#### Example of the configuration for a normal failover time profile: + +| Parameter | Value | +|----------------------------------------|-----------| +| `FAILOTVER_TIMEOUT` | `180000 ` | +| `FAILOVER_WRITER_RECONNECT_INTERVAL` | `2000` | +| `FAILOVER_READER_CONNECT_TIMEOUT` | `30000` | +| `FAILOVER_TOPOLOGY_REFRESH_RATE` | `2000` | + +#### Example of the configuration for an aggressive failover time profile: + +| Parameter | Value | +|----------------------------------------|---------| +| `FAILOTVER_TIMEOUT` | `30000` | +| `FAILOVER_WRITER_RECONNECT_INTERVAL` | `2000` | +| `FAILOVER_READER_CONNECT_TIMEOUT` | `10000` | +| `FAILOVER_TOPOLOGY_REFRESH_RATE` | `2000` | + +### Writer Cluster Endpoints After Failover + +Connecting to a writer cluster endpoint after failover can result in a faulty connection because DNS causes a delay in changing the writer cluster. On the AWS DNS server, this change is updated usually between 15-20 seconds, but the other DNS servers sitting between the application and the AWS DNS server may not be updated in time. Using this stale DNS data will most likely cause problems for users, so it is important to keep this is mind. + +### 2-Node Clusters + +Using failover with a 2-node cluster is not beneficial because during the failover process involving one writer node and one reader node, the two nodes simply switch roles; the reader becomes the writer and the writer becomes the reader. If failover is triggered because one of the nodes has a problem, this problem will persist because there aren't any extra nodes to take the responsibility of the one that is broken. Three or more database nodes are recommended to improve the stability of the cluster. + +### Node Availability + +It seems as though just one node, the one triggering the failover, will be unavailable during the failover process; this is actually not true. When failover is triggered, all nodes become unavailable for a short time. This is because the control plane, which orchestrates the failover process, first shuts down all nodes, then starts the writer node, and finally starts and connects the remaining nodes to the writer. In short, failover requires each node to be reconfigured and thus, all nodes must become unavailable for a short period of time. One additional note to point out is that if your failover time is aggressive, then this may cause failover to fail because some nodes may still be unavailable by the time your failover times out. + +## Enhanced Failure Monitoring + +
+ +The figure above shows a simplified workflow of Enhanced Failure Monitoring (EFM). Enhanced Failure Monitoring is a feature that is available within the MySQL Connector/ODBC Driver. There is a monitor that will periodically check the connected database node's health, or availability. If a database node is determined to be unhealthy, the connection will be aborted. This check uses the [Enhanced Failure Monitoring Parameters](#enhanced-failure-monitoring-parameters) and a database node's responsiveness to determine whether a node is healthy. + +### The Benefits Enhanced Failure Monitoring + +Enhanced Failure Monitoring helps user applications detect failures earlier. When a user application executes a query, EFM may detect that the connected database node is unavailable. When this happens, the query is cancelled and the connection will be aborted. This allows queries to fail fast instead of waiting indefinitely or failing due to a timeout. + +One use case is to pair EFM with [Failover](#failover-specific-options). When EFM discovers a database node failure, the connection will be aborted. Without Failover enabled, the connection would be terminated up to the user application level. If Failover is enabled, the MySQL Connector/ODBC Driver can attempt to failover to a different, healthy database node where the query can be executed. + +Not all user applications will have a need for Enhanced Failure Monitoring. If a user application's query times are predictable and short, and the application does not execute any long-running SQL queries, Enhanced Failure Monitoring may be replaced with a simple network timeout that would consume fewer resources and would be simpler to configure. + +Although this is a viable alternative, EFM is more configurable than simple network timeouts. Users should keep these advantages and disadvantages in mind when deciding whether Enhanced Failure Monitoring is suitable for their application. + +#### Simple Network Timeout + +This option is useful when a user application executes quick statements that run for predictable lengths of time. In this case, the network timeout should be set to a value such as the 95th to 99th percentile. One can set network timeout by setting the `NETWORK_TIMEOUT` property in the `odbc.ini` file, or appending `NETWORK_TIMEOUT=?;` to the connection string (where `?` indicates the timeout value in seconds) or in the UI to configure the DSN. + +### Enabling Host Monitoring + +Enhanced Failure Monitoring is enabled by default in MySQL Connector/ODBC Driver, but it can be disabled with the parameter `ENABLE_FAILURE_DETECTION` is set to `0` in `odbc.ini` or in the connection string. +> +### Enhanced Failure Monitoring Parameters + +
+The parameters `FAILURE_DETECTION_TIME`, `FAILURE_DETECTION_INTERVAL`, and `FAILURE_DETECTION_COUNT` are similar to TCP Keepalive parameters. Each connection has its own set of parameters. The `FAILURE_DETECTION_TIME` is how long the monitor waits after a SQL query is started to send a probe to a database node. The `FAILURE_DETECTION_INTERVAL` is how often the monitor sends a probe to a database node. The `FAILURE_DETECTION_COUNT` is how many times a monitor probe can go unacknowledged before the database node is deemed unhealthy. +The parameter `FAILURE_DETECTION_TIMEOUT` is how long the monitor waits for the probe finishes, and every connection to the same server endpoint will have the same timeout, which is set during the first connection to that server. + +To determine the health of a database node: +1. The monitor will first wait for a time equivalent to the `FAILURE_DETECTION_TIME`. +2. Then, every `FAILURE_DETECTION_INTERVAL`, the monitor will send a probe to the database node. +3. The monitor waits for the probe coming back up to `FAILURE_DETECTION_TIMEOUT` seconds. +4. If the probe is not acknowledged by the database node OR the monitor has timed out, a counter is incremented. +5. If the counter reaches the `FAILURE_DETECTION_COUNT`, the database node will be deemed unhealthy and the connection will be aborted. + +If a more aggressive approach to failure checking is necessary, all of these parameters can be reduced to reflect that. However, increased failure checking may also lead to an increase in false positives. For example, if the `FAILURE_DETECTION_INTERVAL` was shortened, the plugin may complete several connection checks that all fail. The database node would then be considered unhealthy, but it may have been about to recover and the connection checks were completed before that could happen. + +To configure failure detection, you can specify the following parameters in a DSN or connection string. If the values for these options are not specified, the default values will be used. If you are dealing with the Windows DSN UI, click `Details >>` and navigate to the `Monitoring` tab to find the equivalent parameters. + +| Option | Description | Type | Required | Default | +| ---------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------|------------------------------------------| +| `ENABLE_FAILURE_DETECTION` | Set to `1` to enable the fast failover behaviour offered by the MySQL Connector/ODBC Driver for MySQL. | bool | No | `1` | +| `FAILURE_DETECTION_TIME` | Interval in milliseconds between sending a SQL query to the server and the first probe to the database node. | int | No | `30000` | +| `FAILURE_DETECTION_INTERVAL` | Interval in milliseconds between probes to database node. | int | No | `5000` | +| `FAILURE_DETECTION_COUNT` | Number of failed connection checks before considering database node as unhealthy. | int | No | `3` | +| `FAILURE_DETECTION_TIMEOUT` | Amount of time the monitor waits for the probe before timing out. | int | No | `5` | +| `MONITOR_DISPOSAL_TIME` | Interval in milliseconds for a monitor to be considered inactive and to be disposed. | int | No | `60000` | + +> :heavy_exclamation_mark: **Always ensure you provide a non-zero network timeout value or a connect timeout value in your DSN** +> +> MySQL Connector/ODBC Driver has a default non-zero value for `NETWORK_TIMEOUT` and `CONNECT_TIMEOUT`. If one decides to alter those values and set those values to 0, EFM may wait forever to establish a monitoring connection in the event where the database node is unavailable. As a general rule, **do not** override those values to 0. +>### :warning: Warnings About Usage of the MySQL Connector/ODBC Driver with RDS Proxy +> It is recommended to either disable Host Monitoring, or to avoid using RDS Proxy endpoints when the Host Monitoring is enabled. +> +> Although using RDS Proxy endpoints with the Enhanced Failure Monitoring doesn't cause any critical issues, this approach is not recommended. The main reason is that RDS Proxy transparently re-routes requests to one database instance. RDS Proxy decides which database instance is used based on many criteria, and it's on a per-request basis. Such switching between different instances makes Host Monitoring useless in terms of instance health monitoring. It will not be able to identify what actual instance it is connected to and which one it's monitoring. That could be a source of false positive failure detections. At the same time, it could still proactively monitor network connectivity to RDS Proxy endpoints and report outages back to a user application if they occur. + +### Failover Exception Codes + +#### 08S01 - Communication Link Failure + +When the driver returns an error code ```08S01```, the original connection failed, and the driver tried to failover to a new instance, but was not able to. There are various reasons this may happen: no nodes were available, a network failure occurred, and so on. In this scenario, please wait until the server is up or other problems are solved. + +#### 08S02 - Communication Link Changed + +When the driver returns an error code ```08S02```, the original connection failed while autocommit was set to true, and the driver successfully failed over to another available instance in the cluster. However, any session state configuration of the initial connection is now lost. In this scenario, you should: + +1. Reconfigure and reuse the original connection (the reconfigured session state will be the same as the original connection). +2. Repeat the query that was executed when the connection failed and continue work as desired. + +##### Sample Code + +```cpp +#include +#include +#include + +#define MAX_NAME_LEN 255 +#define SQL_MAX_MESSAGE_LENGTH 512 + +// Scenario 1: Failover happens when autocommit is set to true - SQLRETURN with code 08S02. + +void setInitialSessionState(SQLHDBC dbc, SQLHSTMT handle) { + const auto set_timezone = (SQLCHAR*)"SET time_zone = \"+00:00\""; + SQLExecDirect(handle, set_timezone, SQL_NTS); +} + +bool executeQuery(SQLHDBC dbc, SQLCHAR* query) { + int MAX_RETRIES = 5; + SQLHSTMT handle; + SQLCHAR sqlstate[6], message[SQL_MAX_MESSAGE_LENGTH]; + SQLRETURN ret; + + SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle); + + int retries = 0; + bool success = false; + + while (true) { + ret = SQLExecDirect(handle, query, SQL_NTS); + + if (SQL_SUCCEEDED(ret)) { + success = true; + break; + } + else { + if (retries > MAX_RETRIES) { + break; + } + + // Check what kind of error has occurred + SQLSMALLINT stmt_length; + SQLINTEGER native_error; + SQLError(nullptr, nullptr, handle, sqlstate, &native_error, message, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length); + const std::string state = (char*)sqlstate; + + // Failover has occurred and the driver has failed over to another instance successfully + if (state.compare("08S02") == 0) { + // Reconfigure the connection + setInitialSessionState(dbc, handle); + // Re-execute that query again + retries++; + } + else { + // If other exceptions occur + break; + } + } + } + + SQLFreeHandle(SQL_HANDLE_STMT, handle); + return success; +} + +int main() { + SQLHENV env; + SQLHDBC dbc; + SQLSMALLINT len; + SQLRETURN ret; + SQLCHAR conn_in[4096], conn_out[4096]; + + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + + const char* dsn = "ODBCDriverDSN"; + const char* user = "username"; + const char* pwd = "password"; + const char* server = "db-identifier-cluster-XYZ.us-east-2.rds.amazonaws.com"; + int port = 3306; + const char* db = "employees"; + + sprintf(reinterpret_cast(conn_in), "DSN=%s;UID=%s;PWD=%s;SERVER=%s;PORT=%d;DATABASE=%s;", dsn, user, pwd, server, port, db); + + // attempt a connection + ret = SQLDriverConnect(dbc, nullptr, conn_in, SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT); + + if (SQL_SUCCEEDED(ret)) { + // if the connection is successful, execute a query using the MySQL Connector/ODBC Driver + const auto sleep_stmt = (SQLCHAR*)"SELECT * from employees*"; + executeQuery(dbc, sleep_stmt); + } + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); + + return 0; +} +``` + +#### 08007 - Connection Failure During Transaction + +When the driver returns an error code ```08007```, the original connection failed within a transaction (while autocommit was set to false). In this scenario, when the transaction ends, the driver first attempts to rollback the transaction, and then fails over to another available instance in the cluster. Note that the rollback might be unsuccessful as the initial connection may be broken at the time that the driver recognizes the problem. Note also that any session state configuration of the initial connection is now lost. In this scenario, you should: + +1. Reconfigure and reuse the original connection (the reconfigured session state will be the same as the original connection). +2. Re-start the transaction and repeat all queries which were executed during the transaction before the connection failed. +3. Repeat the query that was executed when the connection failed, and continue work. + +##### Sample Code + +```cpp +#include +#include +#include + +#define MAX_NAME_LEN 255 +#define SQL_MAX_MESSAGE_LENGTH 512 + +// Scenario 2: Failover happens when autocommit is set to false - SQLRETURN with code 08007. + +void setInitialSessionState(SQLHDBC dbc, SQLHSTMT handle) { + const auto set_timezone = (SQLCHAR*)"SET time_zone = \"+00:00\""; + const auto setup_autocommit_query = (SQLCHAR*)"SET autocommit = 0"; + + SQLExecDirect(handle, set_timezone, SQL_NTS); + SQLExecDirect(handle, setup_autocommit_query, SQL_NTS); +} + +bool executeQuery(SQLHDBC dbc) { + int MAX_RETRIES = 5; + SQLHSTMT handle; + SQLCHAR sqlstate[6], message[SQL_MAX_MESSAGE_LENGTH]; + SQLRETURN ret; + + // Queries to execute in a transaction + SQLCHAR* queries[] = { + (SQLCHAR*)"INSERT INTO employees(emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES (5000000, '1958-05-01', 'John', 'Doe', 'M', '1997-11-30')", + (SQLCHAR*)"INSERT INTO employees(emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES (5000001, '1958-05-01', 'Mary', 'Malcolm', 'F', '1997-11-30')", + (SQLCHAR*)"INSERT INTO employees(emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES (5000002, '1958-05-01', 'Tom', 'Jerry', 'M', '1997-11-30')" + }; + + SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle); + setInitialSessionState(dbc, handle); + + int retries = 0; + bool success = false; + + while (true) { + for (SQLCHAR* query : queries) { + ret = SQLExecDirect(handle, query, SQL_NTS); + } + SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT); + + if (SQL_SUCCEEDED(ret)) { + success = true; + break; + } else { + if (retries > MAX_RETRIES) { + break; + } + + // Check what kind of error has occurred + SQLSMALLINT stmt_length; + SQLINTEGER native_error; + SQLError(nullptr, nullptr, handle, sqlstate, &native_error, message, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length); + const std::string state = (char*)sqlstate; + + // Failover has occurred and the driver has failed over to another instance successfully + if (state.compare("08007") == 0) { + // Reconfigure the connection + setInitialSessionState(dbc, handle); + // Re-execute every queriy that was inside the transaction + retries++; + } + else { + // If other exceptions occur + break; + } + } + } + + return success; +} + +int main() { + SQLHENV env; + SQLHDBC dbc; + SQLSMALLINT len; + SQLRETURN rc; + SQLCHAR conn_in[4096], conn_out[4096]; + + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + + const char* dsn = "ODBCDriverDSN"; + const char* user = "username"; + const char* pwd = "password"; + const char* server = "db-identifier-cluster-XYZ.us-east-2.rds.amazonaws.com"; + int port = 3306; + const char* db = "employees"; + + sprintf(reinterpret_cast(conn_in), "DSN=%s;UID=%s;PWD=%s;SERVER=%s;PORT=%d;DATABASE=%s;", dsn, user, pwd, server, port, db); + + // attempt a connection + rc = SQLDriverConnect(dbc, nullptr, conn_in, SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT); + + if (SQL_SUCCEEDED(rc)) { + // if the connection is successful, execute a query using the MySQL Connector/ODBC Driver + executeQuery(dbc); + } + + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); + + return 0; +} +``` + +>### :warning: Warnings About Proper Usage of the MySQL Connector/ODBC Driver +>It is highly recommended that you use the cluster and read-only cluster endpoints instead of the direct instance endpoints of your Aurora cluster, unless you are confident about your application's use of instance endpoints. Although the driver will correctly failover to the new writer instance when using instance endpoints, use of these endpoints is discouraged because individual instances can spontaneously change reader/writer status when failover occurs. The driver will always connect directly to the instance specified if an instance endpoint is provided, so a write-safe connection cannot be assumed if the application uses instance endpoints. + +## Building the MySQL Connector/ODBC Driver + +### Windows + + +1. Install the following programs to build the driver: + - [CMake](https://cmake.org/download/) + - [Visual Studio](https://visualstudio.microsoft.com/downloads/) + > The driver has been built successfully using `Visual Studio 2019`, and it may not build correctly with other versions. When installing Visual Studio, ensure the `Visual C++ 2019` and `Visual Studio Tools for CMake` packages are also installed. + - [MySQL Server](https://dev.mysql.com/downloads/installer/) +2. Build the driver in the `build` directory with the following commands: + ``` + cmake -S . -B build -G "Visual Studio 16 2019" -DMYSQL_DIR="C:\Program Files\MySQL\MySQL Server 8.0" -DMYSQLCLIENT_STATIC_LINKING=TRUE + cmake --build build --config Release + ``` + +### MacOS + +1. Install the following packages available on `Homebrew` or other package management system of your choice: + - `libiodbc` + - `cmake` + - `mysql-client` + - `mysql` +2. Set the environment variable MYSQL_DIR as the path to your `mysql-client` installation location: + ``` + export MYSQL_DIR=/usr/local/opt/mysql-client + ``` +3. Build the driver in the `build` directory with the following commands: + ``` + cmake -S . -B build -G "Unix Makefiles" -DMYSQLCLIENT_STATIC_LINKING=true -DODBC_INCLUDES=/usr/local/Cellar/libiodbc/3.52.15/include + cmake --build build --config Release + ``` + Note: you may have a different `libiodbc` version. Change `3.52.15` to your respective version. + +#### Troubleshoot + +If you encounter an `ld: library not found for -lzstd` error, run the following command, and then rebuild the driver: +``` +export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix zstd)/lib/ +``` + +If you encounter an `ld: library not found for -lssl` error, run one of the following commands (depending on what openssl library you have) and then rebuild the driver: +``` +export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl/lib/ +``` +or +``` +export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl@1.1/lib/ +``` + +### Linux + +1. Install the following required packages: + ``` + sudo apt-get update + sudo apt-get install build-essential libgtk-3-dev libmysqlclient-dev unixodbc unixodbc-dev + ``` +2. Build the driver in the `build` directory with the following commands: + ``` + cmake -S . -B build -G "Unix Makefiles" -DMYSQLCLIENT_STATIC_LINKING=true -DWITH_UNIXODBC=1 + cmake --build build --config Release + ``` + +## Testing the MySQL Connector/ODBC Driver + +### Unit Tests + +1. Build driver binaries with `ENABLE_UNIT_TESTS` command set to `TRUE`: + - **Windows** + ``` + cmake -S . -B build -G "Visual Studio 16 2019" -DMYSQL_DIR="C:\Program Files\MySQL\MySQL Server 8.0" -DMYSQLCLIENT_STATIC_LINKING=TRUE -DENABLE_UNIT_TESTS=TRUE + cmake --build build --config Release + ``` + - **MacOS** + ``` + cmake -S . -B build -G "Unix Makefiles" -DMYSQLCLIENT_STATIC_LINKING=true -DODBC_INCLUDES=/usr/local/Cellar/libiodbc/3.52.15/include -DENABLE_UNIT_TESTS=TRUE + cmake --build build --config Release + ``` + - **Linux** + ``` + cmake -S . -B build -G "Unix Makefiles" -DMYSQLCLIENT_STATIC_LINKING=true -DWITH_UNIXODBC=1 -DENABLE_UNIT_TESTS=TRUE + cmake --build build --config Release + ``` +2. There are two options to run the unit tests: + - Run `ctest` directly from the `unit_testing` directory. + - Navigate to `unit_testing/bin/Release` and run `unit_testing.exe`. To specify a particular test or test suite, include `--gtest_filter` in the command. + +The following example demonstrates running all the tests in the `TopologyServiceTest` suite with the `.\unit_testing.exe --gtest_filter=TopologyServiceTest.*` command: + +``` +PS C:\Other\dev\mysql-connector-odbc\unit_testing\bin\Release> .\unit_testing.exe --gtest_filter=TopologyServiceTest.* +Running main() from C:\Other\dev\mysql-connector-odbc\_deps\googletest-src\googletest\src\gtest_main.cc +Note: Google Test filter = TopologyServiceTest.* +[==========] Running 7 tests from 1 test suite. +[----------] Global test environment set-up. +[----------] 7 tests from TopologyServiceTest +[ RUN ] TopologyServiceTest.TopologyQuery +[ OK ] TopologyServiceTest.TopologyQuery (0 ms) +[ RUN ] TopologyServiceTest.MultiWriter +[ OK ] TopologyServiceTest.MultiWriter (0 ms) +[ RUN ] TopologyServiceTest.CachedTopology +[ OK ] TopologyServiceTest.CachedTopology (0 ms) +[ RUN ] TopologyServiceTest.QueryFailure +[ OK ] TopologyServiceTest.QueryFailure (0 ms) +[ RUN ] TopologyServiceTest.StaleTopology +[ OK ] TopologyServiceTest.StaleTopology (1007 ms) +[ RUN ] TopologyServiceTest.RefreshTopology +[ OK ] TopologyServiceTest.RefreshTopology (1013 ms) +[ RUN ] TopologyServiceTest.ClearCache +[ OK ] TopologyServiceTest.ClearCache (0 ms) +[----------] 7 tests from TopologyServiceTest (2026 ms total) + +[----------] Global test environment tear-down +[==========] 7 tests from 1 test suite ran. (2030 ms total) +[ PASSED ] 7 tests. +``` + +### Integration Tests + +There are two types of integration tests you can run. One type is an integration test against a MySQL Server, and the other type consists of the two sets of integration tests specific to the failover functionality provided by the MySQL Connector/ODBC Driver. + +#### Integration Tests Against A MySQL Server + +##### Prerequisites + +- Install MySQL Server. See the [build instructions for the desired system](#building-the-mysql-connector/odbc-driver) for instructions. +- [**Optional**] Install [Docker](https://docs.docker.com/get-docker/). + +##### Steps + +1. Specify the following environment variables on your target platform before building the driver: + | Environment Variable | Description | Example | Platforms | + |----------------------|-----------------------------------------------------------------|---------------------------------|-------------------------| + | TEST_DSN | The DSN to use for the test | ODBCDriverDSN | All systems | + | TEST_USERNAME | The name of the user with access to the MySQL Server | root | All systems | + | TEST_PASSWORD | The password for the test database user | root | All systems | + | TEST_DATABASE | The test database | test | All systems | + | DYLD_LIBRARY_PATH | The path to the library folder of your MySQL server directory | /usr/local/opt/mysql-client/lib | MacOS systems | + | ODBCINI | The path to your odbc.ini file | /etc/odbc.ini | MacOS and Linux systems | + | ODBCINSTINI | The path to your odbcinst.ini file | /etc/odbcinst.ini | MacOS and Linux systems | + > **NOTE:** The `TEST_PASSWORD` environment variable is only required if you have specified a password for the `root` user when installing the MySQL Server. +2. Build and install the driver for a specific platform as described in [Installing the MySQL Connector/ODBC Driver](#installing-the-mysql-connector/odbc-driver). +3. Start the MySQL Server. You may either start a local server or use a docker images. +4. [**Optional**] To start the MySQL Server via a Docker image. Navigate to `test/docker` and execute `docker-compose up -d` to start the server in the background. +5. Navigate to the `test` directory and execute `ctest`. + +#### Failover-specific Integration Tests + +> **NOTE:** This set of tests can only be run on Linux at the moment. + +##### Prerequisites + +- Install JDK 8: + ``` + sudo apt-get install openjdk-8-jdk + ``` +- Install [Docker](https://docs.docker.com/get-docker/) + +##### Steps + +> **NOTE:** Running these tests will automatically create an Amazon Aurora MySQL DB cluster with at least 5 instances and may induce a cost. Ensure the test cluster is cleaned up after testing on the [Amazon RDS Management Console](https://console.aws.amazon.com/rds/home). + +1. This set of tests runs against an Amazon Aurora MySQL DB cluster with at least 5 instances. The test will automatically generate the required AWS MySQL DB cluster and instances if proper AWS credentials are set up. Refer to the [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_SettingUp_Aurora.html) for information about setting up a development environment for Amazon Aurora. +2. Define the following environment variables: + | Environment Variable | Description | Example | + |----------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------| + | TEST_DSN | The DSN to use for the test. | ODBCDriverDSN | + | TEST_USERNAME | The name of the user with access to the Amazon Aurora MySQL DB cluster. | username | + | TEST_PASSWORD | The password for the test database user. | password | + | TEST_DB_CLUSTER_IDENTIFIER | The unique identifier for the Amazon Aurora MySQL DB cluster. | db-identifier | + | AWS_ACCESS_KEY_ID | The access key ID to your AWS account. | `ASIAIOSFODNN7EXAMPLE` | + | AWS_SECRET_ACCESS_KEY | The secret access key for your AWS account. | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` | + | AWS_SESSION_TOKEN | The AWS session token for your AWS count. This is only required if you have set up temporary security credentials. | `AQoDYXdzEJr...` | + | DRIVER_PATH | The directory where the driver was built to. | `~/dev/mysql-connector-odbc/build` | +3. Ensure the following packages are installed on Linux: + ``` + sudo apt-get update && sudo apt-get install \ + build-essential \ + libgtk-3-dev \ + libmysqlclient-dev \ + unixodbc \ + unixodbc-dev \ + curl \ + libcurl4-openssl-dev + ``` +4. Build the driver with the following commands (the following commands assume the driver source code is stored within `~/dev/mysql-connector-odbc`): + ``` + cmake -E make_directory ~/dev/mysql-connector-odbc/build + cmake -S . -B build \ + -G "Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DMYSQLCLIENT_STATIC_LINKING=TRUE \ + -DENABLE_INTEGRATION_TESTS=TRUE \ + -DWITH_UNIXODBC=1 + cmake --build build --config Release + ``` +5. Navigate to the `testframework` directory and run the command: `./gradlew --no-parallel --no-daemon test-failover --info`. +6. Log files are written to the `build` directory as `myodbc.log`. + +## Getting Help and Opening Issues + +If you encounter a bug with the MySQL Connector/ODBC Driver, we would like to hear about it. Please search the [existing issues](https://github.com/mysql/mysql-connector-odbc/issues) and see if others are also experiencing the issue before opening a new issue. When opening a new issue, we will need the version of MySQL Connector/ODBC Driver, C++ language version, OS you’re using, and the MySQL database version you're running against. Please include a reproduction case for the issue when appropriate. + +The GitHub issues are intended for bug reports and feature requests. Keeping the list of open issues lean will help us respond in a timely manner. + +### Logging + +If you encounter an issue with the MySQL Connector/ODBC Driver and would like to report it, please include driver logs if possible, as they help us diagnose problems quicker. + +#### Enabling Logs On Windows + +When connecting the MySQL Connector/ODBC Driver using a Windows system, ensure logging is enabled by following the steps below: + +1. Open the ODBC Data Source Administrator. +2. Add a new DSN or configure an existing DSN of your choice. +3. Open the details for the DSN. +4. Navigate to the Debug tab. +5. Ensure the box to log queries is checked. + +##### Example + +![enable-logging-windows](docs/images/enable_logging_windows.jpg) + +The resulting log file, named `myodbc.log`, can be found under `%temp%`. + +#### Enabling Logs On MacOS and Linux + +When connecting the MySQL Connector/ODBC Driver using a MacOS or Linux system, include the `LOG_QUERY` parameter in the connection string with the value of `1` to enable logging (`DSN=XXX;LOG_QUERY=1;...`). A log file, named `myodbc.log`, will be produced. On MacOS, the log file can be located in `/tmp`. On Linux, the log file can be found in the current working directory. + +## License + +This software is released under version 2 of the GNU General Public License (GPLv2). \ No newline at end of file diff --git a/README.txt b/README.txt index 2086e102..6dba8acc 100644 --- a/README.txt +++ b/README.txt @@ -7,10 +7,34 @@ License information can be found in the LICENSE file. This distribution may include materials developed by third parties. For license and attribution notices for these materials, please refer to the LICENSE file. -For more information on MySQL Connector/ODBC visit http://dev.mysql.com/doc/connector-odbc/en -For additional downloads and the source of MySQL Connector/ODBC visit http://dev.mysql.com/downloads +ABOUT THE DRIVER +================ MySQL Connector/ODBC is brought to you by the MySQL team at Oracle. +The MySQL Connector/ODBC Driver allows an application to take advantage of the features of +clustered MySQL databases. + +-- What is Failover? +An Amazon Aurora DB cluster uses failover to automatically repair the DB cluster +status when a primary DB instance becomes unavailable. During failover, Aurora +promotes a replica to become the new primary DB instance, so that the DB cluster +can provide maximum availability to a primary read-write DB instance. The MySQL +ODBC Driver is designed to coordinate with this behavior in order to +provide minimal downtime in the event of a DB instance failure. + +-- Benefits of the MySQL Connector/ODBC Driver +Although Aurora is able to provide maximum availability through the use of failover, +existing client drivers do not fully support this functionality. This is partially +due to the time required for the DNS of the new primary DB instance to be fully +resolved in order to properly direct the connection. The MySQL ODBC Driver +fully utilizes failover behavior by maintaining a cache of the Aurora cluster +topology and each DB instance's role (Aurora Replica or primary DB instance). This +topology is provided via a direct query to the Aurora database, essentially providing +a shortcut to bypass the delays caused by DNS resolution. With this knowledge, the +MySQL Connector/ODBC Driver can more closely monitor the Aurora DB cluster status so that a +connection to the new primary DB instance can be established as fast as possible. +Additionally, the MySQL ODBC Driver can be used to interact with RDS and +MySQL databases as well as Aurora MySQL. DOCUMENTATION LOCATION ====================== @@ -22,6 +46,9 @@ For the new features/bugfix history, see release notes at . Note that the initial releases used major version 2.0. +For more documentation, visit . +This page contain details on how the driver works and how to use it. + CONTACT ======= @@ -32,3 +59,7 @@ the MySQL Connector/ODBC mailing list at . Bugs can be reported at . Please use the "Connector / ODBC" or "Connector / ODBC Documentation" bug category. + +LICENSE +======= +This software is released under version 2 of the GNU General Public License (GPLv2). diff --git a/build_installer.ps1 b/build_installer.ps1 new file mode 100644 index 00000000..423066fd --- /dev/null +++ b/build_installer.ps1 @@ -0,0 +1,88 @@ +<# +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0 +(GPLv2), as published by the Free Software Foundation, with the +following additional permissions: + +This program is distributed with certain software that is licensed +under separate terms, as designated in a particular file or component +or in the license documentation. Without limiting your rights under +the GPLv2, the authors of this program hereby grant you an additional +permission to link the program and your derivative works with the +separately licensed software that they have included with the program. + +Without limiting the foregoing grant of rights under the GPLv2 and +additional permission as to separately licensed software, this +program is also subject to the Universal FOSS Exception, version 1.0, +a copy of which can be found along with its FAQ at +http://oss.oracle.com/licenses/universal-foss-exception. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +http://www.gnu.org/licenses/gpl-2.0.html. +/#> + +<# +This script is to assist with building the driver and creating the installer. There are two optional arguments: +- $CONFIGURATION: determines what configuration you would like to build the driver in (Debug, Release, RelWithDebInfo) +- $MYSQL_DIR: determines where your MySQL installation is located + +The current default behaviour is to build the driver and installer without unit or integration tests on the Release configuration, +with "C:\Program Files\MySQL\MySQL Server 8.0" set as the location of the MySQL directory + +Note that building the installer requires the following: +- Wix 3.0 or above (https://wixtoolset.org/) +- Microsoft Visual Studio environment +- CMake 2.4.6 (http://www.cmake.org) +#> + +$ARCHITECTURE = $args[0] +$CONFIGURATION = $args[1] +$MYSQL_DIR = $args[2] + +# Set default values +if ($null -eq $CONFIGURATION) { + $CONFIGURATION = "Release" +} +if ($null -eq $MYSQL_DIR) { + $MYSQL_DIR = "C:\Program Files\MySQL\MySQL Server 8.0" +} + +# BUILD DRIVER +cmake -S . -G "Visual Studio 16 2019" -DMYSQL_DIR="$MYSQL_DIR" -DMYSQLCLIENT_STATIC_LINKING=TRUE -DCMAKE_BUILD_TYPE="$CONFIGURATION" -DBUNDLE_DEPENDENCIES=TRUE +cmake --build . --config "$CONFIGURATION" + +# CREATE INSTALLER +# Copy dll, installer, and info files to wix folder +New-Item -Path .\Wix\x64 -ItemType Directory -Force +New-Item -Path .\Wix\x86 -ItemType Directory -Force +New-Item -Path .\Wix\doc -ItemType Directory -Force +Copy-Item .\lib\$CONFIGURATION\myodbc8*.dll .\Wix\x64 +Copy-Item .\lib\$CONFIGURATION\myodbc8*.lib .\Wix\x64 +Copy-Item .\lib\$CONFIGURATION\myodbc8*.dll .\Wix\x86 +Copy-Item .\lib\$CONFIGURATION\myodbc8*.lib .\Wix\x86 +Copy-Item .\bin\$CONFIGURATION\myodbc-installer.exe .\Wix\x64 +Copy-Item .\bin\$CONFIGURATION\myodbc-installer.exe .\Wix\x86 +Copy-Item .\INFO_BIN .\Wix\doc +Copy-Item .\INFO_SRC .\Wix\doc +Copy-Item .\ChangeLog .\Wix\doc +Copy-Item .\README.md .\Wix\doc +Copy-Item .\LICENSE.txt .\Wix\doc + +Set-Location .\Wix +if ($ARCHITECTURE -eq "x64") { + cmake -DMSI_64=1 -G "NMake Makefiles" +} +else { + cmake -DMSI_64=0 -G "NMake Makefiles" +} +nmake + +Set-Location ..\ diff --git a/docs/images/efm_monitor_process.png b/docs/images/efm_monitor_process.png new file mode 100644 index 0000000000000000000000000000000000000000..46c6cfb5ef105e560f3312b9292cfe1e43319171 GIT binary patch literal 40047 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 Nzoes5pkx{R{{UH@7BT<; literal 0 HcmV?d00001 diff --git a/docs/images/enable_logging_windows.jpg b/docs/images/enable_logging_windows.jpg new file mode 100644 index 0000000000000000000000000000000000000000..849bbad88f5aaa62238c1cc4e4e2e8976895e72b GIT binary patch literal 34355 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%0AomWh`in2ThzFC&DQ*iE0LN2Ea$+V zVW++#Q2QcNkw|J_tIiv5X7__jp59sDjJ2?A_stBb9P!5TM*Xm9T{;l)rcx?^3t%n6{I5<`#7| z+mw|_#1erAeZhLZ`$@t96ZpG55TBUTaIVZr>>(WOJbEZd>f1G1^aSoWd^OivXV_;# z&|>JRfwS%VDyO8gBuWz)Yc7mp5IqJMXu{GF9Up{GQo3bLI3?y+8dPjg!SRZ-a!>z| zWnZy1b_e*Weme~EUS~j{Wil_<38>NUpmy!D0EC~|sbZ<~JTEnMVlx%~L`SfGDDd^g za+qB?2Vgoli^bd<{H}4hqG}|}f}-5GRz%X5Cd2^4>6-3e9B7AZ{;1xKrkKXdh|HDa QKS!lY-v5rlvmdYj1DszHH~;_u literal 0 HcmV?d00001 diff --git a/docs/images/enhanced_failure_monitoring_diagram.png b/docs/images/enhanced_failure_monitoring_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7208591b86aee7db608e85c6e0e75fa12695f0 GIT binary patch literal 50709 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 GzWN{LpF1l6 literal 0 HcmV?d00001 diff --git a/docs/images/failover_behaviour.jpg b/docs/images/failover_behaviour.jpg new file mode 100644 index 0000000000000000000000000000000000000000..742612b2ce8e5660ec58e1740f7389ef50e3875d GIT binary patch literal 393459 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%0D`#&pG>a9VZ*Hr3pl)%-hUUFl3Hs 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

b;qUjCol?m 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 Z|L3wg|9X1)*V(^n;Q#R&5c+NSzX2V0{2l-R literal 0 HcmV?d00001 diff --git a/docs/images/failover_diagram.png b/docs/images/failover_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..1c952f8fdd5fbc9a227116e05a897b86550431bb GIT binary patch literal 48011 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
  1. $CRdz6dt~`-Giv{bWt9K4dAicjyQ$z8hBI&%3YI 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 Ul@(Gg0RN1Rnd;{sxft-j0NdzL!2kdN literal 0 HcmV?d00001 diff --git a/driver/CMakeLists.txt b/driver/CMakeLists.txt index e2a3e724..cdab2388 100644 --- a/driver/CMakeLists.txt +++ b/driver/CMakeLists.txt @@ -1,3 +1,5 @@ +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify @@ -55,9 +57,15 @@ WHILE(${DRIVER_INDEX} LESS ${DRIVERS_COUNT}) SET(DRIVER_NAME "myodbc8${CONNECTOR_DRIVER_TYPE_SHORT}") SET(DRIVER_SRCS - catalog.cc catalog_no_i_s.cc connect.cc cursor.cc desc.cc dll.cc error.cc execute.cc - handle.cc info.cc driver.cc options.cc parse.cc prepare.cc results.cc transact.cc - my_prepared_stmt.cc my_stmt.cc utility.cc) + base_metrics_holder.cc catalog.cc catalog_no_i_s.cc cluster_topology_info.cc + cluster_aware_hit_metrics_holder.cc cluster_aware_metrics_container.cc + cluster_aware_metrics.cc cluster_aware_time_metrics_holder.cc + connect.cc cursor.cc desc.cc dll.cc driver.cc + error.cc execute.cc failover_connection_handler.cc failover_handler.cc + failover_reader_handler.cc failover_writer_handler.cc handle.cc host_info.cc info.cc + monitor.cc monitor_connection_context.cc monitor_service.cc monitor_thread_container.cc + my_prepared_stmt.cc my_stmt.cc mylog.cc mysql_proxy.cc options.cc parse.cc prepare.cc query_parsing.cc + results.cc topology_service.cc transact.cc utility.cc) IF(UNICODE) SET(DRIVER_SRCS ${DRIVER_SRCS} unicode.cc) @@ -73,8 +81,12 @@ WHILE(${DRIVER_INDEX} LESS ${DRIVERS_COUNT}) # Headers added for convenience of VS users CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/driver/driver.def.cmake ${CMAKE_SOURCE_DIR}/driver/driver${CONNECTOR_DRIVER_TYPE_SHORT}.def @ONLY) CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/driver/driver.rc.cmake ${CMAKE_SOURCE_DIR}/driver/driver${CONNECTOR_DRIVER_TYPE_SHORT}.rc @ONLY) - SET(DRIVER_SRCS ${DRIVER_SRCS} driver${CONNECTOR_DRIVER_TYPE_SHORT}.def driver${CONNECTOR_DRIVER_TYPE_SHORT}.rc catalog.h driver.h - error.h myutil.h parse.h ../MYODBC_MYSQL.h ../MYODBC_CONF.h ../MYODBC_ODBC.h) + SET(DRIVER_SRCS ${DRIVER_SRCS} driver${CONNECTOR_DRIVER_TYPE_SHORT}.def driver${CONNECTOR_DRIVER_TYPE_SHORT}.rc + base_metrics_holder.h catalog.h cluster_aware_hit_metrics_holder.h cluster_aware_metrics_container.h + cluster_aware_metrics.h cluster_aware_time_metrics_holder.h cluster_topology_info.h + driver.h error.h failover.h host_info.h monitor.h monitor_connection_context.h monitor_service.h + monitor_thread_container.h mylog.h mysql_proxy.h myutil.h parse.h query_parsing.h topology_service.h + ../MYODBC_MYSQL.h ../MYODBC_CONF.h ../MYODBC_ODBC.h) ENDIF(WIN32) # Note: We build driver as a MODULE, because this is what it really is @@ -82,9 +94,16 @@ WHILE(${DRIVER_INDEX} LESS ${DRIVERS_COUNT}) # but a dynamic module that will be loaded by ODBC manager. One # consequence of this is that on Windows import libraries will not # be generated nor installed. + IF(WIN32) + IF(ENABLE_UNIT_TESTS) + ADD_LIBRARY(${DRIVER_NAME} ${DRIVER_SRCS}) + ELSE(ENABLE_UNIT_TESTS) + ADD_LIBRARY(${DRIVER_NAME} SHARED ${DRIVER_SRCS}) + ENDIF(ENABLE_UNIT_TESTS) + ELSE(WIN32) + ADD_LIBRARY(${DRIVER_NAME} SHARED ${DRIVER_SRCS}) + ENDIF(WIN32) - ADD_LIBRARY(${DRIVER_NAME} MODULE ${DRIVER_SRCS}) - ADD_COVERAGE(${DRIVER_NAME}) IF(WIN32) diff --git a/driver/ansi.cc b/driver/ansi.cc index 89f62338..4c501904 100644 --- a/driver/ansi.cc +++ b/driver/ansi.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -86,12 +88,10 @@ SQLColAttributeImpl(SQLHSTMT hstmt, SQLUSMALLINT column, STMT *stmt= (STMT *)hstmt; SQLCHAR *value= NULL; SQLINTEGER len= SQL_NTS; - uint errors; SQLRETURN rc= MySQLColAttribute(hstmt, column, field, &value, num_attr); if (value) { - SQLCHAR *old_value= value; len= strlen((char *)value); /* We set the error only when the result is intented to be returned */ @@ -202,7 +202,6 @@ SQLDescribeCol(SQLHSTMT hstmt, SQLUSMALLINT column, SQLCHAR *value= NULL; SQLINTEGER len= SQL_NTS; SQLSMALLINT free_value= 0; - uint errors; SQLRETURN rc; @@ -213,13 +212,12 @@ SQLDescribeCol(SQLHSTMT hstmt, SQLUSMALLINT column, if (free_value == -1) { - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } if (value) { - SQLCHAR *old_value= value; len= strlen((char *)value); /* We set the error only when the result is intented to be returned */ @@ -289,7 +287,6 @@ SQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, SQLCHAR *in, SQLSMALLINT in_len, if ((rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) && out && out_max) #endif { - uint errors; /* Now we have to convert SQLWCHAR back into a SQLCHAR. */ *out_len= (SQLSMALLINT)sqlwchar_as_sqlchar_buf(default_charset_info, out, out_max, outw, *out_len, &errors); @@ -405,7 +402,6 @@ SQLGetConnectAttrImpl(SQLHDBC hdbc, SQLINTEGER attribute, SQLPOINTER value, if (char_value) { SQLINTEGER len= SQL_NTS; - uint errors; len= strlen((char *)char_value); @@ -447,7 +443,6 @@ SQLGetCursorName(SQLHSTMT hstmt, SQLCHAR *cursor, SQLSMALLINT cursor_max, STMT *stmt= (STMT *)hstmt; SQLCHAR *name; SQLINTEGER len; - uint errors; LOCK_STMT(stmt); CLEAR_STMT_ERROR(stmt); @@ -506,7 +501,6 @@ SQLGetDiagField(SQLSMALLINT handle_type, SQLHANDLE handle, if (value) { - uint errors; len= strlen((char *)value); /* We set the error only when the result is intented to be returned */ @@ -548,7 +542,6 @@ SQLGetDiagRecImpl(SQLSMALLINT handle_type, SQLHANDLE handle, DBC *dbc; SQLCHAR *msg_value= NULL, *sqlstate_value= NULL; SQLINTEGER len= SQL_NTS; - uint errors; if (handle == NULL) { @@ -616,7 +609,6 @@ SQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT type, SQLPOINTER value, DBC *dbc= (DBC *)hdbc; SQLCHAR *char_value= NULL; SQLINTEGER len= SQL_NTS; - uint errors; SQLRETURN rc; @@ -718,8 +710,6 @@ SQLRETURN SQL_API SQLPrepareImpl(SQLHSTMT hstmt, SQLCHAR *str, SQLINTEGER str_len, bool force_prepare) { - STMT *stmt= (STMT *)hstmt; - /* If the ANSI character set is the same as the connection character set, we can pass it straight through. Otherwise it needs to be converted to @@ -805,10 +795,7 @@ SQLRETURN SQL_API SQLSetConnectAttrImpl(SQLHDBC hdbc, SQLINTEGER attribute, SQLPOINTER value, SQLINTEGER value_len) { - SQLRETURN rc; - DBC *dbc= (DBC *)hdbc; - rc= MySQLSetConnectAttr(hdbc, attribute, value, value_len); - return rc; + return MySQLSetConnectAttr(hdbc, attribute, value, value_len); } diff --git a/driver/base_metrics_holder.cc b/driver/base_metrics_holder.cc new file mode 100644 index 00000000..1d42d8ee --- /dev/null +++ b/driver/base_metrics_holder.cc @@ -0,0 +1,200 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "base_metrics_holder.h" + +BASE_METRICS_HOLDER::BASE_METRICS_HOLDER() {} + +BASE_METRICS_HOLDER::~BASE_METRICS_HOLDER() { + delete perf_metrics_hist_breakpoints; + perf_metrics_hist_breakpoints = nullptr; + + delete perf_metrics_hist_counts; + perf_metrics_hist_counts = nullptr; + + delete old_hist_breakpoints; + old_hist_breakpoints = nullptr; + + delete old_hist_counts; + old_hist_counts = nullptr; +} + +void BASE_METRICS_HOLDER::register_query_execution_time(long long query_time_ms) { + if (query_time_ms > longest_query_time_ms) { + longest_query_time_ms = query_time_ms; + + repartition_performance_histogram(); + } + + add_to_performance_histogram(query_time_ms, 1); + + if (query_time_ms < shortest_query_time_ms) { + shortest_query_time_ms = (query_time_ms == 0) ? 1 : query_time_ms; + } + + number_of_queries_issued++; + + total_query_time_ms += query_time_ms; +} + +std::string BASE_METRICS_HOLDER::report_metrics() { + std::string log_message = ""; + + log_message.append("** Performance Metrics Report **\n"); + log_message.append("\nLongest reported query: " + std::to_string(longest_query_time_ms) + " ms"); + log_message.append("\nShortest reported query: " + std::to_string(shortest_query_time_ms) + " ms"); + log_message.append("\nAverage query execution time: " + std::to_string(total_query_time_ms / number_of_queries_issued) + " ms"); + log_message.append("\nNumber of statements executed: " + number_of_queries_issued); + + if (perf_metrics_hist_breakpoints) { + log_message.append("\n\n\tTiming Histogram:\n"); + int max_num_points = 20; + int highest_count = INT_MIN; + + for (int i = 0; i < (HISTOGRAM_BUCKETS); i++) { + if (perf_metrics_hist_counts[i] > highest_count) { + highest_count = perf_metrics_hist_counts[i]; + } + } + + if (highest_count == 0) { + highest_count = 1; // avoid DIV/0 + } + + for (int i = 0; i < (HISTOGRAM_BUCKETS - 1); i++) { + + if (i == 0) { + log_message.append("\n\tless than " + std::to_string(perf_metrics_hist_breakpoints[i + 1]) + " ms: \t" + std::to_string(perf_metrics_hist_counts[i])); + } else { + log_message.append("\n\tbetween " + std::to_string(perf_metrics_hist_breakpoints[i]) + " and " + std::to_string(perf_metrics_hist_breakpoints[i + 1]) + " ms: \t" + + std::to_string(perf_metrics_hist_counts[i])); + } + + log_message.append("\t"); + + int numPointsToGraph = (int) (max_num_points * ((double) perf_metrics_hist_counts[i] / highest_count)); + + for (int j = 0; j < numPointsToGraph; j++) { + log_message.append("*"); + } + + if (longest_query_time_ms < perf_metrics_hist_counts[i + 1]) { + break; + } + } + + if (perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 2] < longest_query_time_ms) { + log_message.append("\n\tbetween "); + log_message.append(std::to_string(perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 2])); + log_message.append(" and "); + log_message.append(std::to_string(perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 1])); + log_message.append(" ms: \t"); + log_message.append(std::to_string(perf_metrics_hist_counts[HISTOGRAM_BUCKETS - 1])); + } + } + + return log_message; +} + +void BASE_METRICS_HOLDER::create_initial_histogram(long long* breakpoints, long long lower_bound, long long upper_bound) { + long long bucket_size = (long long)((((double) upper_bound - (double) lower_bound) / HISTOGRAM_BUCKETS) * 1.25); + + if (bucket_size < 1) { + bucket_size = 1; + } + + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) { + breakpoints[i] = lower_bound; + lower_bound += bucket_size; + } +} + +void BASE_METRICS_HOLDER::add_to_histogram( + int* histogram_counts, + long long* histogram_breakpoints, + long long value, + int number_of_times, + long long current_lower_bound, + long long current_upper_bound) { + + if (!histogram_counts) { + create_initial_histogram(histogram_breakpoints, current_lower_bound, current_upper_bound); + } else { + for (int i = 1; i < HISTOGRAM_BUCKETS; i++) { + if (histogram_breakpoints[i] >= value) { + histogram_counts[i - 1] += number_of_times; + break; + } + } + } +} + +void BASE_METRICS_HOLDER::add_to_performance_histogram(long long value, int number_of_times) { + check_and_create_performance_histogram(); + + add_to_histogram(perf_metrics_hist_counts, perf_metrics_hist_breakpoints, value, number_of_times, + shortest_query_time_ms == LLONG_MAX ? 0 : shortest_query_time_ms, longest_query_time_ms); +} + +void BASE_METRICS_HOLDER::check_and_create_performance_histogram() { + if (!perf_metrics_hist_counts) { + perf_metrics_hist_counts = new int[HISTOGRAM_BUCKETS](); + } + + if (!perf_metrics_hist_breakpoints) { + perf_metrics_hist_breakpoints = new long long[HISTOGRAM_BUCKETS](); + } +} + +void BASE_METRICS_HOLDER::repartition_histogram( + int* hist_counts, + long long* hist_breakpoints, + long long current_lower_bound, + long long current_upper_bound) { + + if (!old_hist_counts) { + old_hist_counts = new int[HISTOGRAM_BUCKETS]; + old_hist_breakpoints = new long[HISTOGRAM_BUCKETS]; + } + memcpy(old_hist_counts, hist_counts, HISTOGRAM_BUCKETS * sizeof(int)); + memcpy(old_hist_breakpoints, hist_breakpoints, HISTOGRAM_BUCKETS * sizeof(long)); + + create_initial_histogram(hist_breakpoints, current_lower_bound, current_upper_bound); + + for (int i = 0; i < HISTOGRAM_BUCKETS; i++) { + add_to_histogram(hist_counts, hist_breakpoints, old_hist_breakpoints[i], old_hist_counts[i], current_lower_bound, current_upper_bound); + } +} + +void BASE_METRICS_HOLDER::repartition_performance_histogram() { + check_and_create_performance_histogram(); + + repartition_histogram(perf_metrics_hist_counts, perf_metrics_hist_breakpoints, + shortest_query_time_ms == LONG_MAX ? 0 : shortest_query_time_ms, longest_query_time_ms); +} diff --git a/driver/base_metrics_holder.h b/driver/base_metrics_holder.h new file mode 100644 index 00000000..fb982aba --- /dev/null +++ b/driver/base_metrics_holder.h @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __BASEMETRICSHOLDER_H__ +#define __BASEMETRICSHOLDER_H__ + +#include +#include +#include +#include + +#include "mylog.h" + +class BASE_METRICS_HOLDER { +public: + BASE_METRICS_HOLDER(); + ~BASE_METRICS_HOLDER(); + + virtual void register_query_execution_time(long long query_time_ms); + virtual std::string report_metrics(); + +private: + void create_initial_histogram(long long* breakpoints, long long lower_bound, long long upper_bound); + void add_to_histogram( + int* histogram_counts, + long long* histogram_breakpoints, + long long value, + int number_of_times, + long long current_lower_bound, + long long current_upper_bound); + void add_to_performance_histogram(long long value, int number_of_times); + void check_and_create_performance_histogram(); + void repartition_histogram( + int* hist_counts, + long long* hist_breakpoints, + long long current_lower_bound, + long long current_upper_bound); + void repartition_performance_histogram(); + +protected: + const static int HISTOGRAM_BUCKETS = 20; + long long longest_query_time_ms = 0; + long number_of_queries_issued = 0; + long* old_hist_breakpoints = nullptr; + int* old_hist_counts = nullptr; + long long shortest_query_time_ms = LONG_MAX; + double total_query_time_ms = 0; + long long* perf_metrics_hist_breakpoints = nullptr; + int* perf_metrics_hist_counts = nullptr; +}; + +#endif /* __BASEMETRICSHOLDER_H__ */ diff --git a/driver/catalog.cc b/driver/catalog.cc index 7988879e..bd2d6859 100644 --- a/driver/catalog.cc +++ b/driver/catalog.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -158,7 +160,7 @@ create_fake_resultset(STMT *stmt, MYSQL_ROW rowval, size_t rowsize, else { if (stmt->result) - mysql_free_result(stmt->result); + stmt->dbc->mysql_proxy->free_result(stmt->result); } /* Free if result data was not in row storage */ @@ -176,7 +178,7 @@ create_fake_resultset(STMT *stmt, MYSQL_ROW rowval, size_t rowsize, x_free(stmt->result); x_free(stmt->result_array); - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } stmt->fake_result= 1; @@ -223,7 +225,6 @@ create_empty_fake_resultset(STMT *stmt, MYSQL_ROW rowval, size_t rowsize, */ MYSQL_RES *db_status(STMT *stmt, std::string &db) { - MYSQL *mysql= stmt->dbc->mysql; /** the buffer size should count possible escapes */ my_bool clause_added= FALSE; std::string query; @@ -249,14 +250,14 @@ MYSQL_RES *db_status(STMT *stmt, std::string &db) query.append(" ORDER BY SCHEMA_NAME"); - MYLOG_QUERY(stmt, query.c_str()); + MYLOG_STMT_TRACE(stmt, query.c_str()); if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) { return NULL; } - return mysql_store_result(mysql); + return stmt->dbc->mysql_proxy->store_result(); } @@ -284,7 +285,7 @@ static MYSQL_RES *table_status_i_s(STMT *stmt, my_bool show_tables, my_bool show_views) { - MYSQL *mysql= stmt->dbc->mysql; + MYSQL_PROXY *mysql_proxy= stmt->dbc->mysql_proxy; /** the buffer size should count possible escapes */ my_bool clause_added= FALSE; std::string query; @@ -343,7 +344,7 @@ static MYSQL_RES *table_status_i_s(STMT *stmt, query.append("AND TABLE_NAME LIKE '"); if (wildcard) { - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)table_name, table_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)table_name, table_len); query.append(tmpbuff, cnt); } else @@ -357,14 +358,14 @@ static MYSQL_RES *table_status_i_s(STMT *stmt, query.append(" ORDER BY TABLE_SCHEMA, TABLE_NAME"); - MYLOG_QUERY(stmt, query.c_str()); + MYLOG_STMT_TRACE(stmt, query.c_str()); if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) { return NULL; } - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -380,7 +381,6 @@ static MYSQL_RES *table_status_i_s(STMT *stmt, @param[in] wildcard Whether the table name is a wildcard @return Result of SHOW TABLE STATUS, or NULL if there is an error - or empty result (check mysql_errno(stmt->dbc->mysql) != 0) */ static MYSQL_RES *table_status_i_s_old(STMT *stmt, SQLCHAR *catalog_name, @@ -391,7 +391,7 @@ static MYSQL_RES *table_status_i_s_old(STMT *stmt, my_bool show_tables, my_bool show_views) { - MYSQL *mysql= stmt->dbc->mysql; + MYSQL_PROXY *mysql_proxy= stmt->dbc->mysql_proxy; /** the buffer size should count possible escapes */ my_bool clause_added= FALSE; std::string query; @@ -449,7 +449,7 @@ static MYSQL_RES *table_status_i_s_old(STMT *stmt, query.append("WHERE TABLE_NAME LIKE '"); if (wildcard) { - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)table_name, table_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)table_name, table_len); query.append(tmpbuff, cnt); } else @@ -461,14 +461,14 @@ static MYSQL_RES *table_status_i_s_old(STMT *stmt, query.append("'"); } - MYLOG_QUERY(stmt, query.c_str()); + MYLOG_STMT_TRACE(stmt, query.c_str()); if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) { return NULL; } - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -483,7 +483,6 @@ static MYSQL_RES *table_status_i_s_old(STMT *stmt, @param[in] wildcard Whether the table name is a wildcard @return Result of SHOW TABLE STATUS, or NULL if there is an error - or empty result (check mysql_errno(stmt->dbc->mysql) != 0) */ MYSQL_RES *table_status(STMT *stmt, SQLCHAR *db_name, @@ -529,7 +528,7 @@ int add_name_condition_oa_id(HSTMT hstmt, std::string &query, SQLCHAR * name, query.append("'"); char tmpbuff[1024]; - size_t cnt = mysql_real_escape_string(stmt->dbc->mysql, tmpbuff, (char *)name, name_len); + size_t cnt = stmt->dbc->mysql_proxy->real_escape_string(tmpbuff, (char *)name, name_len); query.append(tmpbuff, cnt); query.append("' "); } @@ -575,8 +574,7 @@ int add_name_condition_pv_id(HSTMT hstmt, std::string &query, SQLCHAR * name, query.append("'"); char tmpbuff[1024]; - size_t cnt = mysql_real_escape_string(stmt->dbc->mysql, tmpbuff, - (char *)name, name_len); + size_t cnt = stmt->dbc->mysql_proxy->real_escape_string(tmpbuff, (char *)name, name_len); query.append(tmpbuff, cnt); query.append("' "); } @@ -822,7 +820,7 @@ columns_i_s(SQLHSTMT hstmt, SQLCHAR *catalog, unsigned long catalog_len, do_bind(params, column, MYSQL_TYPE_STRING, column_len); } query.append(" ORDER BY ORDINAL_POSITION"); - ODBC_STMT local_stmt(stmt->dbc->mysql); + ODBC_STMT local_stmt(stmt->dbc->mysql_proxy); for (size_t i = 0; i < ccount; i++) { @@ -842,7 +840,7 @@ columns_i_s(SQLHSTMT hstmt, SQLCHAR *catalog, unsigned long catalog_len, throw stmt->error; } - MYLOG_QUERY(stmt, query.c_str()); + MYLOG_STMT_TRACE(stmt, query.c_str()); stmt->dbc->execute_prep_stmt(local_stmt, query, params.data(), results.data()); } catch (const MYERROR &e) @@ -858,7 +856,7 @@ columns_i_s(SQLHSTMT hstmt, SQLCHAR *catalog, unsigned long catalog_len, is_access = true; #endif - size_t rows = mysql_stmt_num_rows(local_stmt); + size_t rows = stmt->dbc->mysql_proxy->stmt_num_rows(local_stmt); stmt->m_row_storage.set_size(rows, SQLCOLUMNS_FIELDS); if (rows == 0) { @@ -871,7 +869,7 @@ columns_i_s(SQLHSTMT hstmt, SQLCHAR *catalog, unsigned long catalog_len, std::string db = get_database_name(stmt, catalog, catalog_len, schema, schema_len, false); size_t rnum = 1; - while(!mysql_stmt_fetch(local_stmt)) + while(!stmt->dbc->mysql_proxy->stmt_fetch(local_stmt)) { CAT_SCHEMA_SET(data[0], data[1], db); /* TABLE_NAME */ @@ -1090,8 +1088,6 @@ SQLRETURN list_table_priv_i_s(SQLHSTMT hstmt, SQLSMALLINT table_len) { STMT *stmt=(STMT *) hstmt; - MYSQL *mysql= stmt->dbc->mysql; - char tmpbuff[1024]; SQLRETURN rc; std::string query; query.reserve(1024); @@ -1170,7 +1166,6 @@ static SQLRETURN list_column_priv_i_s(HSTMT hstmt, SQLSMALLINT column_len) { STMT *stmt=(STMT *) hstmt; - MYSQL *mysql= stmt->dbc->mysql; /* 3 names theorethically can have all their characters escaped - thus 6*NAME_LEN */ char tmpbuff[1024]; SQLRETURN rc; @@ -1360,7 +1355,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, SQLSMALLINT fk_table_len) { STMT *stmt=(STMT *) hstmt; - MYSQL *mysql= stmt->dbc->mysql; + MYSQL_PROXY *mysql_proxy= stmt->dbc->mysql_proxy; char tmpbuff[1024]; /* This should be big enough. */ char *update_rule, *delete_rule, *ref_constraints_join; SQLRETURN rc; @@ -1376,7 +1371,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, /* With 5.1, we can use REFERENTIAL_CONSTRAINTS to get even more info. */ - if (is_minimum_version(stmt->dbc->mysql->server_version, "5.1")) + if (is_minimum_version(stmt->dbc->mysql_proxy->get_server_version(), "5.1")) { update_rule= "CASE" " WHEN R.UPDATE_RULE = 'CASCADE' THEN 0" @@ -1445,8 +1440,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, if (!pk_db.empty()) { query.append("'"); - cnt = mysql_real_escape_string(mysql, tmpbuff, pk_db.c_str(), - pk_db.length()); + cnt = mysql_proxy->real_escape_string(tmpbuff, pk_db.c_str(), pk_db.length()); query.append(tmpbuff, cnt); query.append("' "); } @@ -1457,8 +1451,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, query.append("AND A.REFERENCED_TABLE_NAME = '"); - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)pk_table, - pk_table_len); + cnt =mysql_proxy->real_escape_string(tmpbuff, (char *)pk_table, pk_table_len); query.append(tmpbuff, cnt); query.append("' "); @@ -1472,8 +1465,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, if (!fk_db.empty()) { query.append("'"); - cnt = mysql_real_escape_string(mysql, tmpbuff, fk_db.c_str(), - fk_db.length()); + cnt = mysql_proxy->real_escape_string(tmpbuff, fk_db.c_str(), fk_db.length()); query.append(tmpbuff, cnt); query.append("' "); } @@ -1484,8 +1476,7 @@ SQLRETURN foreign_keys_i_s(SQLHSTMT hstmt, query.append("AND A.TABLE_NAME = '"); - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)fk_table, - fk_table_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)fk_table, fk_table_len); query.append(tmpbuff, cnt); query.append("' "); diff --git a/driver/catalog.h b/driver/catalog.h index 8b3e4598..9dcd4ee7 100644 --- a/driver/catalog.h +++ b/driver/catalog.h @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify diff --git a/driver/catalog_no_i_s.cc b/driver/catalog_no_i_s.cc index 03cdbb8d..9c71232b 100644 --- a/driver/catalog_no_i_s.cc +++ b/driver/catalog_no_i_s.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -96,8 +98,8 @@ static my_bool check_table_type(const SQLCHAR *TableType, */ type= strstr(table_type,","); - sprintf(req_type_quoted,"'%s'",req_type); - sprintf(req_type_quoted1,"`%s`",req_type); + snprintf(req_type_quoted, sizeof(req_type_quoted), "'%s'", req_type); + snprintf(req_type_quoted1, sizeof(req_type_quoted1), "`%s`", req_type); while ( type ) { while ( isspace(*(table_type)) ) ++table_type; @@ -143,7 +145,7 @@ static MYSQL_RES *server_list_dbkeys(STMT *stmt, SQLSMALLINT table_len) { DBC *dbc = stmt->dbc; - MYSQL *mysql= dbc->mysql; + MYSQL_PROXY *mysql_proxy= dbc->mysql_proxy; char tmpbuff[1024]; std::string query; query.reserve(1024); @@ -164,10 +166,10 @@ static MYSQL_RES *server_list_dbkeys(STMT *stmt, query.append(tmpbuff, cnt); query.append("`"); - MYLOG_DBC_QUERY(dbc, query.c_str()); + MYLOG_DBC_TRACE(dbc, query.c_str()); if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) return NULL; - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -192,7 +194,7 @@ server_list_dbcolumns(STMT *stmt, { assert(stmt); DBC *dbc= stmt->dbc; - MYSQL *mysql= dbc->mysql; + MYSQL_PROXY *mysql_proxy= dbc->mysql_proxy; MYSQL_RES *result; char buff[NAME_LEN * 2 + 64], column_buff[NAME_LEN * 2 + 64]; @@ -209,7 +211,7 @@ server_list_dbcolumns(STMT *stmt, strncpy(buff, (const char*)szCatalog, cbCatalog); buff[cbCatalog]= '\0'; - if (mysql_select_db(mysql, buff)) + if (mysql_proxy->select_db(buff)) { return NULL; } @@ -220,15 +222,15 @@ server_list_dbcolumns(STMT *stmt, strncpy(column_buff, (const char*)szColumn, cbColumn); column_buff[cbColumn]= '\0'; - result= mysql_list_fields(mysql, buff, column_buff); + result = mysql_proxy->list_fields(buff, column_buff); /* If before this call no database were selected - we cannot revert that */ if (cbCatalog && !dbc->database.empty()) { - if (mysql_select_db( mysql, dbc->database.c_str())) + if (mysql_proxy->select_db(dbc->database.c_str())) { /* Well, probably have to return error here */ - mysql_free_result(result); + mysql_proxy->free_result(result); return NULL; } } @@ -279,7 +281,7 @@ static MYSQL_RES *table_privs_raw_data( STMT * stmt, SQLSMALLINT table_len) { DBC *dbc= stmt->dbc; - MYSQL *mysql= dbc->mysql; + MYSQL_PROXY *mysql_proxy= dbc->mysql_proxy; char tmpbuff[1024]; std::string query; size_t cnt = 0; @@ -288,7 +290,7 @@ static MYSQL_RES *table_privs_raw_data( STMT * stmt, query = "SELECT Db,User,Table_name,Grantor,Table_priv " "FROM mysql.tables_priv WHERE Table_name LIKE '"; - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)table, table_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)table, table_len); query.append(tmpbuff, cnt); query.append("' AND Db = "); @@ -296,7 +298,7 @@ static MYSQL_RES *table_privs_raw_data( STMT * stmt, if (catalog_len) { query.append("'"); - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)catalog, catalog_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)catalog, catalog_len); query.append(tmpbuff, cnt); query.append("'"); } @@ -305,11 +307,11 @@ static MYSQL_RES *table_privs_raw_data( STMT * stmt, query.append(" ORDER BY Db, Table_name, Table_priv, User"); - MYLOG_DBC_QUERY(dbc, query.c_str()); + MYLOG_DBC_TRACE(dbc, query.c_str()); if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) return NULL; - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -353,7 +355,7 @@ static MYSQL_RES *column_privs_raw_data(STMT * stmt, SQLSMALLINT column_len) { DBC *dbc = stmt->dbc; - MYSQL *mysql = dbc->mysql; + MYSQL_PROXY *mysql_proxy = dbc->mysql_proxy; char tmpbuff[1024]; std::string query; @@ -366,14 +368,14 @@ static MYSQL_RES *column_privs_raw_data(STMT * stmt, "FROM mysql.columns_priv AS c, mysql.tables_priv AS t " "WHERE c.Table_name = '"; - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)table, table_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)table, table_len); query.append(tmpbuff, cnt); query.append("' AND c.Db = "); if (catalog_len) { query.append("'"); - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)catalog, catalog_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)catalog, catalog_len); query.append(tmpbuff, cnt); query.append("'"); } @@ -381,7 +383,7 @@ static MYSQL_RES *column_privs_raw_data(STMT * stmt, query.append("DATABASE()"); query.append("AND c.Column_name LIKE '"); - cnt = mysql_real_escape_string(mysql, tmpbuff, (char *)column, column_len); + cnt = mysql_proxy->real_escape_string(tmpbuff, (char *)column, column_len); query.append(tmpbuff, cnt); query.append("' AND c.Table_name = t.Table_name " @@ -390,7 +392,7 @@ static MYSQL_RES *column_privs_raw_data(STMT * stmt, if (exec_stmt_query(stmt, query.c_str(), query.length(), FALSE)) return NULL; - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -426,7 +428,7 @@ Lengths may not be SQL_NTS. @param[in] table_length Length of table name @return Result of SHOW CREATE TABLE , or NULL if there is an error -or empty result (check mysql_errno(stmt->dbc->mysql) != 0) +or empty result (check stmt->dbc->mysql_proxy->error_code() != 0) */ MYSQL_RES *server_show_create_table(STMT *stmt, SQLCHAR *catalog, @@ -434,7 +436,6 @@ MYSQL_RES *server_show_create_table(STMT *stmt, SQLCHAR *table, SQLSMALLINT table_length) { - MYSQL *mysql= stmt->dbc->mysql; char tmpbuff[1024]; std::string query; size_t cnt = 0; @@ -456,15 +457,14 @@ MYSQL_RES *server_show_create_table(STMT *stmt, query.append(" `").append((char *)table).append("`"); } - MYLOG_QUERY(stmt, query.c_str()); - + MYLOG_STMT_TRACE(stmt, query.c_str()); - if (mysql_real_query(mysql, query.c_str(),(unsigned long)query.length())) + if (!SQL_SUCCEEDED(odbc_stmt(stmt->dbc, query.c_str(), (SQLULEN)query.length(), false))) { return NULL; } - return mysql_store_result(mysql); + return stmt->dbc->mysql_proxy->store_result(); } @@ -603,12 +603,12 @@ primary_keys_no_i_s(SQLHSTMT hstmt, if (!stmt->lengths) { - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } row_count= 0; - while ( (row= mysql_fetch_row(stmt->result)) ) + while ((row = stmt->dbc->mysql_proxy->fetch_row(stmt->result))) { if ( row[1][0] == '0' ) /* If unique index */ { @@ -698,23 +698,23 @@ static MYSQL_RES *server_list_proc_params(STMT *stmt, SQLSMALLINT par_name_len) { DBC *dbc = stmt->dbc; - MYSQL *mysql= dbc->mysql; + MYSQL_PROXY *mysql_proxy= dbc->mysql_proxy; char tmpbuf[1024]; std::string qbuff; qbuff.reserve(2048); - auto append_escaped_string = [&mysql, &tmpbuf](std::string &outstr, + auto append_escaped_string = [&mysql_proxy, &tmpbuf](std::string &outstr, SQLCHAR* str, SQLSMALLINT len) { tmpbuf[0] = '\0'; outstr.append("'"); - mysql_real_escape_string(mysql, tmpbuf, (char *)str, len); + mysql_proxy->real_escape_string(tmpbuf, (char *)str, len); outstr.append(tmpbuf).append("'"); }; - if((is_minimum_version(dbc->mysql->server_version, "5.7"))) + if((is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.7"))) { qbuff = "select SPECIFIC_NAME, (IF(ISNULL(PARAMETER_NAME), " "concat('OUT RETURN_VALUE ', DTD_IDENTIFIER), " @@ -763,11 +763,11 @@ static MYSQL_RES *server_list_proc_params(STMT *stmt, qbuff.append(" ORDER BY Db, name"); } - MYLOG_DBC_QUERY(dbc, qbuff.c_str()); + MYLOG_DBC_TRACE(dbc, qbuff.c_str()); if (exec_stmt_query(stmt, qbuff.c_str(), qbuff.length(), FALSE)) return NULL; - return mysql_store_result(mysql); + return mysql_proxy->store_result(); } @@ -817,7 +817,7 @@ procedure_columns_no_i_s(SQLHSTMT hstmt, SQLPROCEDURECOLUMNS_FIELDS); auto &data = stmt->m_row_storage; - while ((row= mysql_fetch_row(proc_list_res))) + while ((row = stmt->dbc->mysql_proxy->fetch_row(proc_list_res))) { char *token; char *param_str; @@ -854,9 +854,6 @@ procedure_columns_no_i_s(SQLHSTMT hstmt, SQLTypeMap *type_map; SQLSMALLINT dec; SQLULEN param_size= 0; - /* temp variables for debugging */ - SQLUINTEGER dec_int= 0; - SQLINTEGER sql_type_int= 0; token= proc_get_param_type(token, (int)strlen(token), &ptype); token= proc_get_param_name(token, (int)strlen(token), (char*)param_name); @@ -871,7 +868,7 @@ procedure_columns_no_i_s(SQLHSTMT hstmt, param_size= proc_get_param_size(param_dbtype, (int)strlen((const char*)param_dbtype), sql_type_index, &dec); - proc_get_param_octet_len(stmt, sql_type_index, param_size, dec, flags, (char*)param_buffer_len); + proc_get_param_octet_len(stmt, sql_type_index, param_size, dec, flags, (char*)param_buffer_len, sizeof(param_buffer_len)); /* PROCEDURE_CAT and PROCEDURE_SCHEMA */ CAT_SCHEMA_SET(data[mypcPROCEDURE_CAT], data[mypcPROCEDURE_SCHEM], row[2]); @@ -902,7 +899,7 @@ procedure_columns_no_i_s(SQLHSTMT hstmt, data[mypcTYPE_NAME]= (const char*)type_map->type_name; } - proc_get_param_col_len(stmt, sql_type_index, param_size, dec, flags, (char*)param_size_buf); + proc_get_param_col_len(stmt, sql_type_index, param_size, dec, flags, (char*)param_size_buf, sizeof(param_size_buf)); data[mypcCOLUMN_SIZE] = (const char*)param_size_buf; data[mypcBUFFER_LENGTH] = (const char*)param_buffer_len; @@ -973,7 +970,7 @@ procedure_columns_no_i_s(SQLHSTMT hstmt, SQLPROCEDURECOLUMNS_fields, SQLPROCEDURECOLUMNS_FIELDS); free_internal_result_buffers(stmt); - mysql_free_result(proc_list_res); + stmt->dbc->mysql_proxy->free_result(proc_list_res); case EXCEPTION_TYPE::GENERAL: break; } @@ -1059,8 +1056,8 @@ special_columns_no_i_s(SQLHSTMT hstmt, SQLUSMALLINT fColType, (SQLSMALLINT colType) { uint f_count = 0; - mysql_field_seek(result,0); - while(field = mysql_fetch_field(result)) + stmt->dbc->mysql_proxy->field_seek(result, 0); + while (field = stmt->dbc->mysql_proxy->fetch_field(result)) { if(colType == SQL_ROWVER) { @@ -1095,7 +1092,7 @@ special_columns_no_i_s(SQLHSTMT hstmt, SQLUSMALLINT fColType, data[3] = buff; /* COLUMN_SIZE */ - fill_column_size_buff(buff, stmt, field); + fill_column_size_buff(buff, sizeof(buff), stmt, field); data[4] = buff; /* BUFFER_LENGTH */ @@ -1143,7 +1140,7 @@ special_columns_no_i_s(SQLHSTMT hstmt, SQLUSMALLINT fColType, /* Check if there is a primary (unique) key */ primary_key= 0; - while ( (field= mysql_fetch_field(result)) ) + while ((field = stmt->dbc->mysql_proxy->fetch_field(result))) { if ( field->flags & PRI_KEY_FLAG ) { @@ -1202,7 +1199,6 @@ statistics_no_i_s(SQLHSTMT hstmt, STMT *stmt= (STMT *)hstmt; assert(stmt); - MYSQL *mysql= stmt->dbc->mysql; DBC *dbc= stmt->dbc; char *db_val = nullptr; std::string db; @@ -1229,7 +1225,7 @@ statistics_no_i_s(SQLHSTMT hstmt, sizeof(SQLSTAT_values),MYF(0)); if (!stmt->array) { - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } @@ -1254,7 +1250,7 @@ statistics_no_i_s(SQLHSTMT hstmt, } } (*prev)= 0; - mysql_data_seek(stmt->result,0); /* Restore pointer */ + stmt->dbc->mysql_proxy->data_seek(stmt->result,0); /* Restore pointer */ } set_row_count(stmt, stmt->result->row_count); @@ -1416,10 +1412,10 @@ tables_no_i_s(SQLHSTMT hstmt, user_tables, views); } - if (!stmt->result && mysql_errno(stmt->dbc->mysql)) + if (!stmt->result && stmt->dbc->mysql_proxy->error_code()) { /* unknown DB will return empty set from SQLTables */ - switch (mysql_errno(stmt->dbc->mysql)) + switch (stmt->dbc->mysql_proxy->error_code()) { case ER_BAD_DB_ERROR: throw ODBCEXCEPTION(EXCEPTION_TYPE::EMPTY_SET); @@ -1442,7 +1438,7 @@ tables_no_i_s(SQLHSTMT hstmt, free_internal_result_buffers(stmt); if (stmt->result) { - mysql_free_result(stmt->result); + stmt->dbc->mysql_proxy->free_result(stmt->result); stmt->result = nullptr; } @@ -1454,7 +1450,7 @@ tables_no_i_s(SQLHSTMT hstmt, stmt->m_row_storage.set_size(row_count, SQLTABLES_FIELDS); int name_index = 0; - while ((row= mysql_fetch_row(stmt->result))) + while ((row = stmt->dbc->mysql_proxy->fetch_row(stmt->result))) { int type_index = 2; int comment_index = 1; diff --git a/driver/cluster_aware_hit_metrics_holder.cc b/driver/cluster_aware_hit_metrics_holder.cc new file mode 100644 index 00000000..6b8cbd9f --- /dev/null +++ b/driver/cluster_aware_hit_metrics_holder.cc @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_aware_hit_metrics_holder.h" + +CLUSTER_AWARE_HIT_METRICS_HOLDER::CLUSTER_AWARE_HIT_METRICS_HOLDER(std::string metric_name):metric_name{metric_name} {} + +CLUSTER_AWARE_HIT_METRICS_HOLDER::~CLUSTER_AWARE_HIT_METRICS_HOLDER() {} + +void CLUSTER_AWARE_HIT_METRICS_HOLDER::register_metrics(bool is_hit) { + number_of_reports++; + if (is_hit) { + number_of_hits++; + } +} + +std::string CLUSTER_AWARE_HIT_METRICS_HOLDER::report_metrics() { + std::string log_message = ""; + + log_message.append("\n\n** Performance Metrics Report for '"); + log_message.append(metric_name); + log_message.append("' **"); + log_message.append("\nNumber of reports: ").append(std::to_string(number_of_reports)); + if (number_of_reports > 0) { + log_message.append("\nNumber of hits: ").append(std::to_string(number_of_hits)); + log_message.append("\nRatio : ").append(std::to_string(number_of_hits * 100.0 / number_of_reports)).append(" %"); + } + + return log_message; +} diff --git a/driver/cluster_aware_hit_metrics_holder.h b/driver/cluster_aware_hit_metrics_holder.h new file mode 100644 index 00000000..c02fa1e3 --- /dev/null +++ b/driver/cluster_aware_hit_metrics_holder.h @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __CLUSTERAWAREHITMETRICSHOLDER_H__ +#define __CLUSTERAWAREHITMETRICSHOLDER_H__ + +#include "mylog.h" + +class CLUSTER_AWARE_HIT_METRICS_HOLDER { + public: + CLUSTER_AWARE_HIT_METRICS_HOLDER(std::string metric_name); + ~CLUSTER_AWARE_HIT_METRICS_HOLDER(); + void register_metrics(bool is_hit); + std::string report_metrics(); + + protected: + std::string metric_name = ""; + int number_of_reports = 0; + int number_of_hits = 0; +}; + +#endif /* __CLUSTERAWAREHITMETRICSHOLDER_H__ */ diff --git a/driver/cluster_aware_metrics.cc b/driver/cluster_aware_metrics.cc new file mode 100644 index 00000000..50e9e4d0 --- /dev/null +++ b/driver/cluster_aware_metrics.cc @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_aware_metrics.h" + +CLUSTER_AWARE_METRICS::CLUSTER_AWARE_METRICS() {} + +CLUSTER_AWARE_METRICS::~CLUSTER_AWARE_METRICS() {} + +void CLUSTER_AWARE_METRICS::register_failure_detection_time(long long time_ms) { + failure_detection->register_query_execution_time(time_ms); +} + +void CLUSTER_AWARE_METRICS::register_writer_failover_procedure_time(long long time_ms) { + writer_failover_procedure->register_query_execution_time(time_ms); +} + +void CLUSTER_AWARE_METRICS::register_reader_failover_procedure_time(long long time_ms) { + reader_failover_procedure->register_query_execution_time(time_ms); +} + +void CLUSTER_AWARE_METRICS::register_topology_query_time(long long time_ms) { + topology_query->register_query_execution_time(time_ms); +} + +void CLUSTER_AWARE_METRICS::register_failover_connects(bool is_hit) { + failover_connects->register_metrics(is_hit); +} + +void CLUSTER_AWARE_METRICS::register_invalid_initial_connection(bool is_hit) { + invalid_initial_connection->register_metrics(is_hit); +} + +void CLUSTER_AWARE_METRICS::register_use_cached_topology(bool is_hit) { + use_cached_topology->register_metrics(is_hit); +} + +std::string CLUSTER_AWARE_METRICS::report_metrics() { + std::string log_message = ""; + log_message.append(failover_connects->report_metrics()); + log_message.append(failure_detection->report_metrics()); + log_message.append(writer_failover_procedure->report_metrics()); + log_message.append(reader_failover_procedure->report_metrics()); + log_message.append(topology_query->report_metrics()); + log_message.append(use_cached_topology->report_metrics()); + log_message.append(invalid_initial_connection->report_metrics()); + return log_message; +} diff --git a/driver/cluster_aware_metrics.h b/driver/cluster_aware_metrics.h new file mode 100644 index 00000000..fd2f03ac --- /dev/null +++ b/driver/cluster_aware_metrics.h @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __CLUSTERAWAREMETRICS_H__ +#define __CLUSTERAWAREMETRICS_H__ + +#include + +#include "cluster_aware_hit_metrics_holder.h" +#include "cluster_aware_time_metrics_holder.h" + +class CLUSTER_AWARE_METRICS { +public: + CLUSTER_AWARE_METRICS(); + ~CLUSTER_AWARE_METRICS(); + void register_failure_detection_time(long long time_ms); + void register_writer_failover_procedure_time(long long time_ms); + void register_reader_failover_procedure_time(long long time_ms); + void register_topology_query_time(long long time_ms); + void register_failover_connects(bool is_hit); + void register_invalid_initial_connection(bool is_hit); + void register_use_cached_topology(bool is_hit); + + std::string report_metrics(); + +private: + std::shared_ptr failure_detection = std::make_shared("Failover Detection"); + std::shared_ptr writer_failover_procedure = std::make_shared("Writer Failover Procedure"); + std::shared_ptr reader_failover_procedure = std::make_shared("Reader Failover Procedure"); + std::shared_ptr topology_query = std::make_shared("Topology Query"); + std::shared_ptr failover_connects = std::make_shared("Successful Failover Reconnects"); + std::shared_ptr invalid_initial_connection = std::make_shared("Invalid Initial Connection"); + std::shared_ptr use_cached_topology = std::make_shared("Used Cached Topology"); +}; + +#endif /* __CLUSTERAWAREMETRICS_H__ */ diff --git a/driver/cluster_aware_metrics_container.cc b/driver/cluster_aware_metrics_container.cc new file mode 100644 index 00000000..b8cff9b5 --- /dev/null +++ b/driver/cluster_aware_metrics_container.cc @@ -0,0 +1,162 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_aware_metrics_container.h" +#include "driver.h" +#include "installer.h" + +std::unordered_map> CLUSTER_AWARE_METRICS_CONTAINER::cluster_metrics = {}; +std::unordered_map> CLUSTER_AWARE_METRICS_CONTAINER::instance_metrics = {}; + +CLUSTER_AWARE_METRICS_CONTAINER::CLUSTER_AWARE_METRICS_CONTAINER() {} + +CLUSTER_AWARE_METRICS_CONTAINER::CLUSTER_AWARE_METRICS_CONTAINER(DBC* dbc, DataSource* ds) { + this->dbc = dbc; + this->ds = ds; +} + +CLUSTER_AWARE_METRICS_CONTAINER::~CLUSTER_AWARE_METRICS_CONTAINER() {} + +void CLUSTER_AWARE_METRICS_CONTAINER::set_cluster_id(std::string id) { + this->cluster_id = id; +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_failure_detection_time(long long time_ms) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([time_ms](std::shared_ptr metrics){metrics->register_failure_detection_time(time_ms);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_writer_failover_procedure_time(long long time_ms) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([time_ms](std::shared_ptr metrics){metrics->register_writer_failover_procedure_time(time_ms);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_reader_failover_procedure_time(long long time_ms) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([time_ms](std::shared_ptr metrics){metrics->register_reader_failover_procedure_time(time_ms);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_failover_connects(bool is_hit) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([is_hit](std::shared_ptr metrics){metrics->register_failover_connects(is_hit);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_invalid_initial_connection(bool is_hit) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([is_hit](std::shared_ptr metrics){metrics->register_invalid_initial_connection(is_hit);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_use_cached_topology(bool is_hit) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([is_hit](std::shared_ptr metrics){metrics->register_use_cached_topology(is_hit);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_topology_query_execution_time(long long time_ms) { + CLUSTER_AWARE_METRICS_CONTAINER::register_metrics([time_ms](std::shared_ptr metrics){metrics->register_topology_query_time(time_ms);}); +} + +void CLUSTER_AWARE_METRICS_CONTAINER::set_gather_metric(bool gather) { + this->can_gather = gather; +} + +void CLUSTER_AWARE_METRICS_CONTAINER::report_metrics(std::string conn_url, bool for_instances, FILE* log, unsigned long dbc_id) { + if (!log) { + return; + } + + MYLOG_TRACE(log, dbc_id, report_metrics(conn_url, for_instances).c_str()); +} + +std::string CLUSTER_AWARE_METRICS_CONTAINER::report_metrics(std::string conn_url, bool for_instances) { + std::string log_message = "\n"; + + std::unordered_map>::const_iterator has_metrics = for_instances ? instance_metrics.find(conn_url) : cluster_metrics.find(conn_url); + if ((for_instances ? instance_metrics.end() : cluster_metrics.end()) == has_metrics) { + log_message.append("** No metrics collected for '") + .append(conn_url) + .append("' **\n"); + + return log_message; + } + + std::shared_ptr metrics = has_metrics->second; + + log_message.append("** Performance Metrics Report for '") + .append(conn_url) + .append("' **\n"); + + log_message.append(metrics->report_metrics()) + .append("\n"); + + return log_message; +} + +void CLUSTER_AWARE_METRICS_CONTAINER::reset_metrics() { + cluster_metrics.clear(); + instance_metrics.clear(); +} + +bool CLUSTER_AWARE_METRICS_CONTAINER::is_enabled() { + if (ds) { + return ds->gather_perf_metrics; + } + return can_gather; +} + +bool CLUSTER_AWARE_METRICS_CONTAINER::is_instance_metrics_enabled() { + if (ds) { + return ds->gather_metrics_per_instance; + } + return can_gather; +} + +std::shared_ptr CLUSTER_AWARE_METRICS_CONTAINER::get_cluster_metrics(std::string key) { + cluster_metrics.emplace(key, std::make_shared()); + return cluster_metrics.at(key); +} + +std::shared_ptr CLUSTER_AWARE_METRICS_CONTAINER::get_instance_metrics(std::string key) { + instance_metrics.emplace(key, std::make_shared()); + return instance_metrics.at(key); +} + +std::string CLUSTER_AWARE_METRICS_CONTAINER::get_curr_conn_url() { + std::string curr_url = "[Unknown Url]"; + if (dbc && dbc->mysql_proxy) { + curr_url = dbc->mysql_proxy->get_host(); + curr_url.append(":").append(std::to_string(dbc->mysql_proxy->get_port())); + } + return curr_url; +} + +void CLUSTER_AWARE_METRICS_CONTAINER::register_metrics(std::function)> lambda) { + if (!is_enabled()) { + return; + } + + lambda(get_cluster_metrics(this->cluster_id)); + + if (is_instance_metrics_enabled()) { + lambda(get_instance_metrics(get_curr_conn_url())); + } +} diff --git a/driver/cluster_aware_metrics_container.h b/driver/cluster_aware_metrics_container.h new file mode 100644 index 00000000..72086afc --- /dev/null +++ b/driver/cluster_aware_metrics_container.h @@ -0,0 +1,89 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __CLUSTERAWAREMETRICSCONTAINER_H__ +#define __CLUSTERAWAREMETRICSCONTAINER_H__ + +#include +#include + +#include "cluster_aware_time_metrics_holder.h" +#include "cluster_aware_metrics.h" +#include "mylog.h" + +#define DEFAULT_CLUSTER_ID "no_id" + +struct DBC; +struct DataSource; + +class CLUSTER_AWARE_METRICS_CONTAINER { +public: + CLUSTER_AWARE_METRICS_CONTAINER(); + CLUSTER_AWARE_METRICS_CONTAINER(DBC* dbc, DataSource* ds); + + ~CLUSTER_AWARE_METRICS_CONTAINER(); + + void set_cluster_id(std::string cluster_id); + void register_failure_detection_time(long long time_ms); + void register_writer_failover_procedure_time(long long time_ms); + void register_reader_failover_procedure_time(long long time_ms); + void register_failover_connects(bool is_hit); + void register_invalid_initial_connection(bool is_hit); + void register_use_cached_topology(bool is_hit); + void register_topology_query_execution_time(long long time_ms); + + void set_gather_metric(bool gather); + + static void report_metrics(std::string conn_url, bool for_instances, FILE* log, unsigned long dbc_id); + static std::string report_metrics(std::string conn_url, bool for_instances); + static void reset_metrics(); + +private: + // ClusterID, Metrics + static std::unordered_map> cluster_metrics; + // Instance URL, Metrics + static std::unordered_map> instance_metrics; + + bool can_gather = false; + DBC* dbc = nullptr; + DataSource* ds = nullptr; + std::string cluster_id = DEFAULT_CLUSTER_ID; + +protected: + bool is_enabled(); + bool is_instance_metrics_enabled(); + + std::shared_ptr get_cluster_metrics(std::string key); + std::shared_ptr get_instance_metrics(std::string key); + virtual std::string get_curr_conn_url(); + + void register_metrics(std::function)> lambda); +}; + +#endif /* __CLUSTERAWAREMETRICSCONTAINER_H__ */ diff --git a/driver/cluster_aware_time_metrics_holder.cc b/driver/cluster_aware_time_metrics_holder.cc new file mode 100644 index 00000000..a464b99f --- /dev/null +++ b/driver/cluster_aware_time_metrics_holder.cc @@ -0,0 +1,108 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_aware_time_metrics_holder.h" + +CLUSTER_AWARE_TIME_METRICS_HOLDER::CLUSTER_AWARE_TIME_METRICS_HOLDER(std::string metric_name):metric_name{metric_name} {} + +CLUSTER_AWARE_TIME_METRICS_HOLDER::~CLUSTER_AWARE_TIME_METRICS_HOLDER() {} + +std::string CLUSTER_AWARE_TIME_METRICS_HOLDER::report_metrics() { + std::string log_message = ""; + + log_message.append("\n\n** Performance Metrics Report for '").append(metric_name).append("' **"); + if (number_of_queries_issued > 0) { + log_message.append("\nLongest reported time: ").append(std::to_string(longest_query_time_ms)).append(" ms"); + log_message.append("\nShortest reported time: ").append(std::to_string(shortest_query_time_ms)).append(" ms"); + double avg_time = total_query_time_ms / number_of_queries_issued; + log_message.append("\nAverage query execution time: ").append(std::to_string(avg_time)).append(" ms"); + } + log_message.append("\nNumber of reports: ").append(std::to_string(number_of_queries_issued)); + + if (number_of_queries_issued > 0 && perf_metrics_hist_breakpoints) { + log_message.append("\n\n\tTiming Histogram:\n"); + int max_num_points = 20; + int highest_count = INT_MIN; + + for (int i = 0; i < (HISTOGRAM_BUCKETS); i++) { + if (perf_metrics_hist_counts[i] > highest_count) { + highest_count = perf_metrics_hist_counts[i]; + } + } + + if (highest_count == 0) { + highest_count = 1; // avoid DIV/0 + } + + for (int i = 0; i < (HISTOGRAM_BUCKETS - 1); i++) { + + if (i == 0) { + log_message.append("\n\tless than ") + .append(std::to_string(perf_metrics_hist_breakpoints[i + 1])) + .append(" ms: \t") + .append(std::to_string(perf_metrics_hist_counts[i])); + } else { + log_message.append("\n\tbetween ") + .append(std::to_string(perf_metrics_hist_breakpoints[i])) + .append(" and ") + .append(std::to_string(perf_metrics_hist_breakpoints[i + 1])) + .append(" ms: \t") + .append(std::to_string(perf_metrics_hist_counts[i])); + } + + log_message.append("\t"); + + int numPointsToGraph = + (int) (max_num_points * ((double) perf_metrics_hist_counts[i] / highest_count)); + + for (int j = 0; j < numPointsToGraph; j++) { + log_message.append("*"); + } + + if (longest_query_time_ms < perf_metrics_hist_counts[i + 1]) { + break; + } + } + + if (perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 2] < longest_query_time_ms) { + log_message.append("\n\tbetween "); + log_message.append(std::to_string(perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 2])); + log_message.append(" and "); + log_message.append(std::to_string(perf_metrics_hist_breakpoints[HISTOGRAM_BUCKETS - 1])); + log_message.append(" ms: \t"); + log_message.append(std::to_string(perf_metrics_hist_counts[HISTOGRAM_BUCKETS - 1])); + } + } + + return log_message; +} + +void CLUSTER_AWARE_TIME_METRICS_HOLDER::register_query_execution_time(long long query_time_ms) { + BASE_METRICS_HOLDER::register_query_execution_time(query_time_ms); +} diff --git a/driver/cluster_aware_time_metrics_holder.h b/driver/cluster_aware_time_metrics_holder.h new file mode 100644 index 00000000..487da37f --- /dev/null +++ b/driver/cluster_aware_time_metrics_holder.h @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __CLUSTERAWARETIMEMETRICSHOLDER_H__ +#define __CLUSTERAWARETIMEMETRICSHOLDER_H__ + +#include "base_metrics_holder.h" + +class CLUSTER_AWARE_TIME_METRICS_HOLDER : public BASE_METRICS_HOLDER { +public: + CLUSTER_AWARE_TIME_METRICS_HOLDER(std::string metric_name); + ~CLUSTER_AWARE_TIME_METRICS_HOLDER(); + + void register_query_execution_time(long long queryTimeMs) override; + std::string report_metrics() override; + +protected: + std::string metric_name = ""; +}; + +#endif /* __CLUSTERAWARETIMEMETRICSHOLDER_H__ */ diff --git a/driver/cluster_topology_info.cc b/driver/cluster_topology_info.cc new file mode 100644 index 00000000..f995fa6c --- /dev/null +++ b/driver/cluster_topology_info.cc @@ -0,0 +1,160 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_topology_info.h" + +#include + +/** + Initialize and return random number. + + Returns random number. + */ +int get_random_number() { + std::srand((unsigned int)time(nullptr)); + return rand(); +} + +CLUSTER_TOPOLOGY_INFO::CLUSTER_TOPOLOGY_INFO() { + update_time(); +} + +// copy constructor +CLUSTER_TOPOLOGY_INFO::CLUSTER_TOPOLOGY_INFO(const CLUSTER_TOPOLOGY_INFO& src_info) + : current_reader{src_info.current_reader}, + last_updated{src_info.last_updated}, + last_used_reader{src_info.last_used_reader}, + is_multi_writer_cluster{src_info.is_multi_writer_cluster} { + for (auto host_info_source : src_info.writers) { + writers.push_back(std::make_shared(*host_info_source)); //default copy + } + for (auto host_info_source : src_info.readers) { + readers.push_back(std::make_shared(*host_info_source)); //default copy + } +} + +CLUSTER_TOPOLOGY_INFO::~CLUSTER_TOPOLOGY_INFO() { + for (auto p : writers) { + p.reset(); + } + writers.clear(); + + for (auto p : readers) { + p.reset(); + } + readers.clear(); +} + +void CLUSTER_TOPOLOGY_INFO::add_host(std::shared_ptr host_info) { + host_info->is_host_writer() ? writers.push_back(host_info) : readers.push_back(host_info); + update_time(); +} + +size_t CLUSTER_TOPOLOGY_INFO::total_hosts() { + return writers.size() + readers.size(); +} + +size_t CLUSTER_TOPOLOGY_INFO::num_readers() { + return readers.size(); +} + +std::time_t CLUSTER_TOPOLOGY_INFO::time_last_updated() { + return last_updated; +} + +// TODO harmonize time function across objects so the times are comparable +void CLUSTER_TOPOLOGY_INFO::update_time() { + last_updated = time(0); +} + +std::shared_ptr CLUSTER_TOPOLOGY_INFO::get_writer() { + if (writers.size() <= 0) { + throw std::runtime_error("No writer available"); + } + + return writers[0]; +} + +std::shared_ptr CLUSTER_TOPOLOGY_INFO::get_next_reader() { + size_t num_readers = readers.size(); + if (num_readers <= 0) { + throw std::runtime_error("No reader available"); + } + + if (current_reader == -1) { // initialize for the first time + current_reader = get_random_number() % num_readers; + } + else if (current_reader >= num_readers) { + // adjust current reader in case topology was refreshed. + current_reader = (current_reader) % num_readers; + } + else { + current_reader = (current_reader + 1) % num_readers; + } + + return readers[current_reader]; +} + +std::shared_ptr CLUSTER_TOPOLOGY_INFO::get_reader(int i) { + if (i < 0 || i >= readers.size()) { + throw std::runtime_error("No reader available at index " + i); + } + + return readers[i]; +} + +std::vector> CLUSTER_TOPOLOGY_INFO::get_readers() { + return readers; +} + +std::vector> CLUSTER_TOPOLOGY_INFO::get_writers() { + return writers; +} + +std::shared_ptr CLUSTER_TOPOLOGY_INFO::get_last_used_reader() { + return last_used_reader; +} + +void CLUSTER_TOPOLOGY_INFO::set_last_used_reader(std::shared_ptr reader) { + last_used_reader = reader; +} + +void CLUSTER_TOPOLOGY_INFO::mark_host_down(std::shared_ptr host) { + host->set_host_state(DOWN); + down_hosts.insert(host->get_host_port_pair()); +} + +void CLUSTER_TOPOLOGY_INFO::mark_host_up(std::shared_ptr host) { + host->set_host_state(UP); + down_hosts.erase(host->get_host_port_pair()); +} + +std::set CLUSTER_TOPOLOGY_INFO::get_down_hosts() { + return down_hosts; +} diff --git a/driver/cluster_topology_info.h b/driver/cluster_topology_info.h new file mode 100644 index 00000000..fe50f2f9 --- /dev/null +++ b/driver/cluster_topology_info.h @@ -0,0 +1,86 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __CLUSTERTOPOLOGYINFO_H__ +#define __CLUSTERTOPOLOGYINFO_H__ + +#include "host_info.h" + +#include +#include +#include + +// This class holds topology information for one cluster. +// Cluster topology consists of an instance endpoint, a set of nodes in the cluster, +// the type of each node in the cluster, and the status of each node in the cluster. +class CLUSTER_TOPOLOGY_INFO { +public: + CLUSTER_TOPOLOGY_INFO(); + CLUSTER_TOPOLOGY_INFO(const CLUSTER_TOPOLOGY_INFO& src_info); //copy constructor + virtual ~CLUSTER_TOPOLOGY_INFO(); + + void add_host(std::shared_ptr host_info); + size_t total_hosts(); + size_t num_readers(); // return number of readers in the cluster + std::time_t time_last_updated(); + + std::shared_ptr get_writer(); + std::shared_ptr get_next_reader(); + // TODO - Ponder if the get_reader below is needed. In general user of this should not need to deal with indexes. + // One case that comes to mind, if we were to try to do a random shuffle of readers or hosts in general like JDBC driver + // we could do random shuffle of host indices and call the get_reader for specific index in order we wanted. + std::shared_ptr get_reader(int i); + std::vector> get_writers(); + std::vector> get_readers(); + bool is_multi_writer_cluster = false; + +private: + int current_reader = -1; + std::time_t last_updated; + std::set down_hosts; // maybe not needed, HOST_INFO has is_host_down() method + //std::vector hosts; + std::shared_ptr last_used_reader; // TODO perhaps this overlaps with current_reader and is not needed + + // TODO - can we do without pointers - + // perhaps ok for now, we are using copies CLUSTER_TOPOLOGY_INFO returned by get_topology and get_cached_topology from TOPOLOGY_SERVICE. + // However, perhaps smart shared pointers could be used. + std::vector> writers; + std::vector> readers; + + std::shared_ptr get_last_used_reader(); + void set_last_used_reader(std::shared_ptr reader); + void mark_host_down(std::shared_ptr host); + void mark_host_up(std::shared_ptr host); + std::set get_down_hosts(); + void update_time(); + + friend class TOPOLOGY_SERVICE; +}; + +#endif /* __CLUSTERTOPOLOGYINFO_H__ */ diff --git a/driver/connect.cc b/driver/connect.cc old mode 100755 new mode 100644 index a2d3738f..d150d65e --- a/driver/connect.cc +++ b/driver/connect.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -33,7 +35,6 @@ #include "driver.h" #include "installer.h" -#include "stringutil.h" #include #include @@ -83,7 +84,7 @@ unsigned long get_client_flags(DataSource *ds) flags|= CLIENT_IGNORE_SPACE; if (ds->allow_multiple_statements) flags|= CLIENT_MULTI_STATEMENTS; - if (ds->clientinteractive) + if (ds->client_interactive) flags|= CLIENT_INTERACTIVE; return flags; @@ -97,7 +98,7 @@ void DBC::set_charset(std::string charset) std::string query = "SET NAMES " + charset; if (odbc_stmt(this, query.c_str(), query.length(), TRUE)) { - throw MYERROR("HY000", mysql); + throw MYERROR("HY000", mysql_proxy); } } @@ -137,7 +138,7 @@ try { MY_CHARSET_INFO my_charset; - mysql_get_character_set_info(mysql, &my_charset); + mysql_proxy->get_character_set_info(&my_charset); cxn_charset_info = get_charset(my_charset.number, MYF(0)); } @@ -212,12 +213,6 @@ struct Prio } }; -struct Srv_host_detail -{ - std::string name; - unsigned int port = MYSQL_PORT; -}; - /* Parse a comma separated list of hosts, each optionally specifying a port after @@ -278,6 +273,34 @@ std::vector parse_host_list(const char* hosts_str, return list; } +std::shared_ptr get_host_info_from_ds(DataSource* ds) { + std::vector hosts; + std::stringstream err; + try { + hosts = + parse_host_list(ds_get_utf8attr(ds->server, &ds->server8), ds->port); + } catch (std::string &) { + err << "Invalid server '" << ds->server8 << "'."; + if (ds->save_queries) { + MYLOG_TRACE(init_log_file().get(), 0, err.str().c_str()); + } + throw std::runtime_error(err.str()); + } + + if (hosts.size() == 0) { + err << "No host was retrieved from the data source."; + if (ds->save_queries) { + MYLOG_TRACE(init_log_file().get(), 0, err.str().c_str()); + } + throw std::runtime_error(err.str()); + } + + std::string main_host(hosts[0].name); + unsigned int main_port = hosts[0].port; + + return std::make_shared(main_host, main_port); +} + class dbc_guard { @@ -294,12 +317,13 @@ class dbc_guard Try to establish a connection to a MySQL server based on the data source configuration. - @param[in] ds Data source information + @param[in] dsrc Data source information + @param[in] failover_enabled Flag for failover @return Standard SQLRETURN code. If it is @c SQL_SUCCESS or @c SQL_SUCCESS_WITH_INFO, a connection has been established. */ -SQLRETURN DBC::connect(DataSource *dsrc) +SQLRETURN DBC::connect(DataSource *dsrc, bool failover_enabled) { SQLRETURN rc = SQL_SUCCESS; unsigned long flags; @@ -307,6 +331,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) unsigned int opt_ssl_verify_server_cert = ~0; const my_bool on = 1; unsigned int on_int = 1; + unsigned int off_int = 0; unsigned long max_long = ~0L; bool initstmt_executed = false; @@ -330,7 +355,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) #endif - mysql = mysql_init(nullptr); + this->mysql_proxy->init(); flags = get_client_flags(dsrc); @@ -338,28 +363,35 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (dsrc->allow_big_results || dsrc->safe) #if MYSQL_VERSION_ID >= 50709 - mysql_options(mysql, MYSQL_OPT_MAX_ALLOWED_PACKET, &max_long); + mysql_proxy->options(MYSQL_OPT_MAX_ALLOWED_PACKET, &max_long); #else /* max_allowed_packet is a magical mysql macro. */ max_allowed_packet = ~0L; #endif if (dsrc->force_use_of_named_pipes) - mysql_options(mysql, MYSQL_OPT_NAMED_PIPE, NullS); + mysql_proxy->options(MYSQL_OPT_NAMED_PIPE, NullS); if (dsrc->read_options_from_mycnf) - mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "odbc"); + mysql_proxy->options(MYSQL_READ_DEFAULT_GROUP, "odbc"); - if (login_timeout) - mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, (char *)&login_timeout); - - if (dsrc->readtimeout) - mysql_options(mysql, MYSQL_OPT_READ_TIMEOUT, - (const char *) &dsrc->readtimeout); + unsigned int connect_timeout, read_timeout, write_timeout; + if (failover_enabled) + { + connect_timeout = get_connect_timeout(dsrc->connect_timeout); + read_timeout = get_network_timeout(dsrc->network_timeout); + write_timeout = read_timeout; + } + else + { + connect_timeout = get_connect_timeout(login_timeout); + read_timeout = get_network_timeout(dsrc->read_timeout); + write_timeout = get_network_timeout(dsrc->write_timeout); + } - if (dsrc->writetimeout) - mysql_options(mysql, MYSQL_OPT_WRITE_TIMEOUT, - (const char *) &dsrc->writetimeout); + mysql_proxy->options(MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout); + mysql_proxy->options(MYSQL_OPT_READ_TIMEOUT, &read_timeout); + mysql_proxy->options(MYSQL_OPT_WRITE_TIMEOUT, &write_timeout); /* Pluggable authentication was introduced in mysql 5.5.7 @@ -367,8 +399,8 @@ SQLRETURN DBC::connect(DataSource *dsrc) #if MYSQL_VERSION_ID >= 50507 if (dsrc->plugin_dir) { - mysql_options(mysql, MYSQL_PLUGIN_DIR, - ds_get_utf8attr(dsrc->plugin_dir, &dsrc->plugin_dir8)); + mysql_proxy->options(MYSQL_PLUGIN_DIR, + ds_get_utf8attr(dsrc->plugin_dir, &dsrc->plugin_dir8)); } #ifdef WIN32 @@ -378,14 +410,14 @@ SQLRETURN DBC::connect(DataSource *dsrc) If plugin directory is not set we can use the dll location for a better chance of finding plugins. */ - mysql_options(mysql, MYSQL_PLUGIN_DIR, default_plugin_location.c_str()); + mysql_proxy->options(MYSQL_PLUGIN_DIR, default_plugin_location.c_str()); } #endif if (dsrc->default_auth) { - mysql_options(mysql, MYSQL_DEFAULT_AUTH, - ds_get_utf8attr(dsrc->default_auth, &dsrc->default_auth8)); + mysql_proxy->options(MYSQL_DEFAULT_AUTH, + ds_get_utf8attr(dsrc->default_auth, &dsrc->default_auth8)); } /* @@ -405,7 +437,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (fido_func || fido_callback_is_set) { struct st_mysql_client_plugin* plugin = - mysql_client_find_plugin(mysql, + mysql_proxy->client_find_plugin( "authentication_fido_client", MYSQL_CLIENT_AUTHENTICATION_PLUGIN); @@ -430,9 +462,8 @@ SQLRETURN DBC::connect(DataSource *dsrc) { /* load client authentication plugin if required */ struct st_mysql_client_plugin *plugin = - mysql_client_find_plugin(mysql, - "authentication_oci_client", - MYSQL_CLIENT_AUTHENTICATION_PLUGIN); + mysql_proxy->client_find_plugin("authentication_oci_client", + MYSQL_CLIENT_AUTHENTICATION_PLUGIN); if(!plugin) { @@ -450,25 +481,24 @@ SQLRETURN DBC::connect(DataSource *dsrc) #endif /* set SSL parameters */ - mysql_ssl_set(mysql, - ds_get_utf8attr(dsrc->sslkey, &dsrc->sslkey8), - ds_get_utf8attr(dsrc->sslcert, &dsrc->sslcert8), - ds_get_utf8attr(dsrc->sslca, &dsrc->sslca8), - ds_get_utf8attr(dsrc->sslcapath, &dsrc->sslcapath8), - ds_get_utf8attr(dsrc->sslcipher, &dsrc->sslcipher8)); + mysql_proxy->ssl_set(ds_get_utf8attr(dsrc->sslkey, &dsrc->sslkey8), + ds_get_utf8attr(dsrc->sslcert, &dsrc->sslcert8), + ds_get_utf8attr(dsrc->sslca, &dsrc->sslca8), + ds_get_utf8attr(dsrc->sslcapath, &dsrc->sslcapath8), + ds_get_utf8attr(dsrc->sslcipher, &dsrc->sslcipher8)); #if MYSQL_VERSION_ID < 80003 if (dsrc->sslverify) - mysql_options(mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, - (const char *)&opt_ssl_verify_server_cert); + mysql_proxy->options(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, + (const char *)&opt_ssl_verify_server_cert); #endif #if MYSQL_VERSION_ID >= 50660 if (dsrc->rsakey) { /* Read the public key on the client side */ - mysql_options(mysql, MYSQL_SERVER_PUBLIC_KEY, - ds_get_utf8attr(dsrc->rsakey, &dsrc->rsakey8)); + mysql_proxy->options(MYSQL_SERVER_PUBLIC_KEY, + ds_get_utf8attr(dsrc->rsakey, &dsrc->rsakey8)); } #endif #if MYSQL_VERSION_ID >= 50710 @@ -499,7 +529,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) } if (!tls_options.length() || - mysql_options(mysql, MYSQL_OPT_TLS_VERSION, tls_options.c_str())) + mysql_proxy->options(MYSQL_OPT_TLS_VERSION, tls_options.c_str())) { return set_error("HY000", "SSL connection error: No valid TLS version available", 0); @@ -509,7 +539,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (dsrc->ssl_crl) { - if (mysql_options(mysql, MYSQL_OPT_SSL_CRL, + if (mysql_proxy->options(MYSQL_OPT_SSL_CRL, ds_get_utf8attr(dsrc->ssl_crl, &dsrc->ssl_crl8))) { return set_error("HY000", "Failed to set the certificate revocation list file", 0); @@ -518,7 +548,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (dsrc->ssl_crlpath) { - if (mysql_options(mysql, MYSQL_OPT_SSL_CRLPATH, + if (mysql_proxy->options(MYSQL_OPT_SSL_CRLPATH, ds_get_utf8attr(dsrc->ssl_crlpath, &dsrc->ssl_crlpath8))) { return set_error("HY000", "Failed to set the certificate revocation list path", 0); @@ -529,7 +559,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (dsrc->get_server_public_key) { /* Get the server public key */ - mysql_options(mysql, MYSQL_OPT_GET_SERVER_PUBLIC_KEY, (const void*)&on); + mysql_proxy->options(MYSQL_OPT_GET_SERVER_PUBLIC_KEY, (const void*)&on); } #endif @@ -539,12 +569,12 @@ SQLRETURN DBC::connect(DataSource *dsrc) Get the ANSI charset info before we change connection to UTF-8. */ MY_CHARSET_INFO my_charset; - mysql_get_character_set_info(mysql, &my_charset); + mysql_proxy->get_character_set_info(&my_charset); ansi_charset_info= get_charset(my_charset.number, MYF(0)); /* We always use utf8 for the connection, and change it afterwards if needed. */ - mysql_options(mysql, MYSQL_SET_CHARSET_NAME, transport_charset); + mysql_proxy->options(MYSQL_SET_CHARSET_NAME, transport_charset); cxn_charset_info= utf8_charset_info; } else @@ -558,12 +588,12 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (client_cs_name) { - mysql_options(mysql, MYSQL_SET_CHARSET_NAME, client_cs_name); + mysql_proxy->options(MYSQL_SET_CHARSET_NAME, client_cs_name); ansi_charset_info= cxn_charset_info= get_charset_by_csname(client_cs_name, MYF(MY_CS_PRIMARY), MYF(0)); } #else MY_CHARSET_INFO my_charset; - mysql_get_character_set_info(mysql, &my_charset); + mysql_proxy->get_character_set_info(&my_charset); ansi_charset_info= get_charset(my_charset.number, MYF(0)); #endif } @@ -571,27 +601,31 @@ SQLRETURN DBC::connect(DataSource *dsrc) #if MYSQL_VERSION_ID >= 50610 if (dsrc->can_handle_exp_pwd) { - mysql_options(mysql, MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS, (char *)&on); + mysql_proxy->options(MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS, (char *)&on); } #endif #if (MYSQL_VERSION_ID >= 50527 && MYSQL_VERSION_ID < 50600) || MYSQL_VERSION_ID >= 50607 if (dsrc->enable_cleartext_plugin) { - mysql_options(mysql, MYSQL_ENABLE_CLEARTEXT_PLUGIN, (char *)&on); + mysql_proxy->options(MYSQL_ENABLE_CLEARTEXT_PLUGIN, (char *)&on); } #endif if (dsrc->enable_local_infile) { - mysql_options(mysql, MYSQL_OPT_LOCAL_INFILE, &on_int); + mysql_proxy->options(MYSQL_OPT_LOCAL_INFILE, &on_int); + } + else + { + mysql_proxy->options(MYSQL_OPT_LOCAL_INFILE, &off_int); } if (dsrc->load_data_local_dir && dsrc->load_data_local_dir[0]) { ds_get_utf8attr(dsrc->load_data_local_dir, &dsrc->load_data_local_dir8); - mysql_options(mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, - dsrc->load_data_local_dir8); + mysql_proxy->options(MYSQL_OPT_LOAD_DATA_LOCAL_DIR, + dsrc->load_data_local_dir8); } #if MFA_ENABLED @@ -599,27 +633,27 @@ SQLRETURN DBC::connect(DataSource *dsrc) { ds_get_utf8attr(dsrc->pwd1, &dsrc->pwd18); int fator = 1; - mysql_options4(mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - dsrc->pwd18); + mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + dsrc->pwd18); } if(dsrc->pwd2 && dsrc->pwd2[0]) { ds_get_utf8attr(dsrc->pwd2, &dsrc->pwd28); int fator = 2; - mysql_options4(mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - dsrc->pwd28); + mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + dsrc->pwd28); } if(dsrc->pwd3 && dsrc->pwd3[0]) { ds_get_utf8attr(dsrc->pwd3, &dsrc->pwd38); int fator = 3; - mysql_options4(mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - dsrc->pwd38); + mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + dsrc->pwd38); } #endif #if MYSQL_VERSION_ID >= 50711 @@ -640,7 +674,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) // Don't do anything if there is no match with any of the available modes if (mode) - mysql_options(mysql, MYSQL_OPT_SSL_MODE, &mode); + mysql_proxy->options(MYSQL_OPT_SSL_MODE, &mode); } #endif @@ -709,36 +743,29 @@ SQLRETURN DBC::connect(DataSource *dsrc) { protocol = MYSQL_PROTOCOL_TCP; } - mysql_options(mysql, MYSQL_OPT_PROTOCOL, &protocol); + mysql_proxy->options(MYSQL_OPT_PROTOCOL, &protocol); //Setting server and port to the dsrc->server8 and dsrc->port - //TODO: IS THERE A FUNCTION TO DO THIS USING myodbc_malloc? - //COPY OF sqlwchardup - size_t chars = strlen(host); - x_free(dsrc->server8); - dsrc->server8 = (SQLCHAR *)myodbc_malloc(( chars + 1) , MYF(0)); - memcpy(dsrc->server8, host, chars); + ds_set_strnattr(&dsrc->server8, (SQLCHAR*)host, strlen(host)); dsrc->port = port; - MYSQL *connect_result = dsrc->enable_dns_srv ? - mysql_real_connect_dns_srv(mysql, - host, + const bool connect_result = dsrc->enable_dns_srv ? + mysql_proxy->real_connect_dns_srv(host, ds_get_utf8attr(dsrc->uid, &dsrc->uid8), ds_get_utf8attr(dsrc->pwd, &dsrc->pwd8), ds_get_utf8attr(dsrc->database, &dsrc->database8), flags) : - mysql_real_connect(mysql, - host, + mysql_proxy->real_connect(host, ds_get_utf8attr(dsrc->uid, &dsrc->uid8), ds_get_utf8attr(dsrc->pwd, &dsrc->pwd8), ds_get_utf8attr(dsrc->database, &dsrc->database8), - port, + port, ds_get_utf8attr(dsrc->socket, &dsrc->socket8), flags); if (!connect_result) { - unsigned int native_error= mysql_errno(mysql); + unsigned int native_error= mysql_proxy->error_code(); /* Before 5.6.11 error returned by server was ER_MUST_CHANGE_PASSWORD(1820). In 5.6.11 it changed to ER_MUST_CHANGE_PASSWORD_LOGIN(1862) @@ -762,7 +789,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) "this functionlaity", 0); } #endif - set_error("HY000", mysql_error(mysql), native_error); + set_error("HY000", mysql_proxy->error(), native_error); translate_error((char*)error.sqlstate.c_str(), MYERR_S1000, native_error); @@ -796,7 +823,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) } else { - switch (mysql_errno(mysql)) + switch (mysql_proxy->error_code()) { case ER_CON_COUNT_ERROR: case CR_SOCKET_CREATE_ERROR: @@ -808,7 +835,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) break; default: //If SQLSTATE not 08xxx, which is used for network errors - if(strncmp(mysql_sqlstate(mysql), "08", 2) != 0) + if(strncmp(mysql_proxy->sqlstate(), "08", 2) != 0) { //Return error and do not try another host return SQL_ERROR; @@ -835,9 +862,9 @@ SQLRETURN DBC::connect(DataSource *dsrc) } } - has_query_attrs = mysql->server_capabilities & CLIENT_QUERY_ATTRIBUTES; + has_query_attrs = mysql_proxy->get_server_capabilities() & CLIENT_QUERY_ATTRIBUTES; - if (!is_minimum_version(mysql->server_version, "4.1.1")) + if (!is_minimum_version(mysql_proxy->get_server_version(), "4.1.1")) { close(); return set_error("08001", "Driver does not support server versions under 4.1.1", 0); @@ -890,17 +917,14 @@ SQLRETURN DBC::connect(DataSource *dsrc) database= ds_get_utf8attr(ds->database, &ds->database8); } - if (ds->save_queries && !query_log) - query_log = init_query_log(); - /* Set the statement error prefix based on the server version. */ strxmov(st_error_prefix, MYODBC_ERROR_PREFIX, "[mysqld-", - mysql->server_version, "]", NullS); + mysql_proxy->get_server_version(), "]", NullS); /* This needs to be set after connection, or it doesn't stick. */ if (ds->auto_reconnect) { - mysql_options(mysql, MYSQL_OPT_RECONNECT, (char *)&on); + mysql_proxy->options(MYSQL_OPT_RECONNECT, (char *)&on); } /* Make sure autocommit is set as configured. */ @@ -914,7 +938,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) "SQL_AUTOCOMMIT_OFF changed to SQL_AUTOCOMMIT_ON", SQL_SUCCESS_WITH_INFO); } - else if (autocommit_is_on() && mysql_autocommit(mysql, FALSE)) + else if (autocommit_is_on() && mysql_proxy->autocommit(FALSE)) { /** @todo set error */ goto error; @@ -923,7 +947,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) else if ((commit_flag == CHECK_AUTOCOMMIT_ON) && transactions_supported() && !autocommit_is_on()) { - if (mysql_autocommit(mysql, TRUE)) + if (mysql_proxy->autocommit(TRUE)) { /** @todo set error */ goto error; @@ -947,7 +971,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) if (transactions_supported()) { - sprintf(buff, "SET SESSION TRANSACTION ISOLATION LEVEL %s", level); + snprintf(buff, sizeof(buff), "SET SESSION TRANSACTION ISOLATION LEVEL %s", level); if (odbc_stmt(this, buff, SQL_NTS, TRUE) != SQL_SUCCESS) { goto error; @@ -962,7 +986,7 @@ SQLRETURN DBC::connect(DataSource *dsrc) } } - mysql_get_option(mysql, MYSQL_OPT_NET_BUFFER_LENGTH, &net_buffer_len); + mysql_proxy->get_option(MYSQL_OPT_NET_BUFFER_LENGTH, &net_buffer_len); guard.set_success(rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO); return rc; @@ -1003,7 +1027,7 @@ SQLRETURN SQL_API MySQLConnect(SQLHDBC hdbc, #else /* Can't connect if we're already connected. */ - if (is_connected(dbc)) + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) return set_conn_error((DBC*)hdbc, MYERR_08002, NULL, 0); /* Reset error state */ @@ -1017,14 +1041,18 @@ SQLRETURN SQL_API MySQLConnect(SQLHDBC hdbc, ds= ds_new(); - ds_set_strnattr(&ds->name, szDSN, cbDSN); - ds_set_strnattr(&ds->uid, szUID, cbUID); - ds_set_strnattr(&ds->pwd, szAuth, cbAuth); + ds_set_wstrnattr(&ds->name, szDSN, cbDSN); + ds_set_wstrnattr(&ds->uid, szUID, cbUID); + ds_set_wstrnattr(&ds->pwd, szAuth, cbAuth); ds_lookup(ds); - rc= dbc->connect(ds); + if (ds->save_queries && !dbc->log_file) + dbc->log_file = init_log_file(); + dbc->mysql_proxy = new MYSQL_PROXY(dbc, ds); + dbc->fh = new FAILOVER_HANDLER(dbc, ds); + rc = dbc->fh->init_cluster_info(); if (!dbc->ds) ds_delete(ds); return rc; @@ -1136,7 +1164,12 @@ SQLRETURN SQL_API MySQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, case SQL_DRIVER_COMPLETE: case SQL_DRIVER_COMPLETE_REQUIRED: - rc = dbc->connect(ds); + if (ds->save_queries && !dbc->log_file) + dbc->log_file = init_log_file(); + + dbc->mysql_proxy = new MYSQL_PROXY(dbc, ds); + dbc->fh = new FAILOVER_HANDLER(dbc, ds); + rc = dbc->fh->init_cluster_info(); if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) goto connected; bPrompt= TRUE; @@ -1181,10 +1214,11 @@ SQLRETURN SQL_API MySQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, if (!ds->driver) { char szError[1024]; - sprintf(szError, - "Could not determine the driver name; " - "could not lookup setup library. DSN=(%s)\n", - ds_get_utf8attr(ds->name, &ds->name8)); + snprintf(szError, + sizeof(szError), + "Could not determine the driver name; " + "could not lookup setup library. DSN=(%s)\n", + ds_get_utf8attr(ds->name, &ds->name8)); rc= dbc->set_error("HY000", szError, 0); goto error; } @@ -1207,8 +1241,8 @@ SQLRETURN SQL_API MySQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, if (driver_lookup(pDriver)) { char sz[1024]; - sprintf(sz, "Could not find driver '%s' in system information.", - ds_get_utf8attr(ds->driver, &ds->driver8)); + snprintf(sz, sizeof(sz), "Could not find driver '%s' in system information.", + ds_get_utf8attr(ds->driver, &ds->driver8)); rc= dbc->set_error("IM003", sz, 0); goto error; @@ -1237,8 +1271,8 @@ SQLRETURN SQL_API MySQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, &pDriver->setup_lib8)))) { char sz[1024]; - sprintf(sz, "Could not load the setup library '%s'.", - ds_get_utf8attr(pDriver->setup_lib, &pDriver->setup_lib8)); + snprintf(sz, sizeof(sz), "Could not load the setup library '%s'.", + ds_get_utf8attr(pDriver->setup_lib, &pDriver->setup_lib8)); rc= dbc->set_error("HY000", sz, 0); goto error; } @@ -1309,7 +1343,12 @@ SQLRETURN SQL_API MySQLDriverConnect(SQLHDBC hdbc, SQLHWND hwnd, } - rc = dbc->connect(ds); + if (ds->save_queries && !dbc->log_file) + dbc->log_file = init_log_file(); + + dbc->mysql_proxy = new MYSQL_PROXY(dbc, ds); + dbc->fh = new FAILOVER_HANDLER(dbc, ds); + rc = dbc->fh->init_cluster_info(); if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { goto error; @@ -1399,6 +1438,23 @@ void DBC::free_connection_stmts() SQLRETURN SQL_API SQLDisconnect(SQLHDBC hdbc) { DBC *dbc= (DBC *) hdbc; + DataSource* ds = dbc->ds; + + if (ds && ds->gather_perf_metrics) { + std::string cluster_id_str = ""; + if (dbc->fh) { + cluster_id_str = dbc->fh->cluster_id; + } + + if (((cluster_id_str == DEFAULT_CLUSTER_ID) || ds->gather_metrics_per_instance) && dbc->mysql_proxy) { + cluster_id_str = dbc->mysql_proxy->get_host(); + cluster_id_str.append(":").append(std::to_string(dbc->mysql_proxy->get_port())); + } + + CLUSTER_AWARE_METRICS_CONTAINER::report_metrics( + cluster_id_str, dbc->ds->gather_metrics_per_instance, + dbc->log_file ? dbc->log_file.get() : nullptr, dbc->id); + } CHECK_HANDLE(hdbc); @@ -1406,8 +1462,10 @@ SQLRETURN SQL_API SQLDisconnect(SQLHDBC hdbc) dbc->close(); - if (dbc->ds && dbc->ds->save_queries) - end_query_log(dbc->query_log); + if (dbc->ds && dbc->ds->save_queries) { + dbc->log_file.reset(); + end_log_file(); + } /* free allocated packet buffer */ @@ -1426,10 +1484,10 @@ void DBC::execute_prep_stmt(MYSQL_STMT *pstmt, std::string &query, MYSQL_BIND *param_bind, MYSQL_BIND *result_bind) { if ( - mysql_stmt_prepare(pstmt, query.c_str(), query.length()) || - (param_bind && mysql_stmt_bind_param(pstmt, param_bind)) || - mysql_stmt_execute(pstmt) || - (result_bind && mysql_stmt_bind_result(pstmt, result_bind)) + mysql_proxy->stmt_prepare(pstmt, query.c_str(), query.length()) || + (param_bind && mysql_proxy->stmt_bind_param(pstmt, param_bind)) || + mysql_proxy->stmt_execute(pstmt) || + (result_bind && mysql_proxy->stmt_bind_result(pstmt, result_bind)) ) { set_error("HY000"); @@ -1438,7 +1496,7 @@ void DBC::execute_prep_stmt(MYSQL_STMT *pstmt, std::string &query, if ( - (result_bind && mysql_stmt_store_result(pstmt)) + (result_bind && mysql_proxy->stmt_store_result(pstmt)) ) { set_error("HY000"); diff --git a/driver/cursor.cc b/driver/cursor.cc index c2d0dc1f..bda25ce0 100644 --- a/driver/cursor.cc +++ b/driver/cursor.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -49,7 +51,8 @@ /* Sets affected rows everewhere where SQLRowCOunt could look for */ void global_set_affected_rows(STMT * stmt, my_ulonglong rows) { - stmt->affected_rows= stmt->dbc->mysql->affected_rows= rows; + stmt->dbc->mysql_proxy->set_affected_rows(rows); + stmt->affected_rows = rows; /* Dirty hack. But not dirtier than the one above */ if (ssps_used(stmt)) @@ -224,21 +227,21 @@ static my_bool check_if_usable_unique_key_exists(STMT *stmt) /* Use SHOW KEYS FROM table to check for keys. */ pos= myodbc_stpmov(buff, "SHOW KEYS FROM `"); - pos+= mysql_real_escape_string(stmt->dbc->mysql, pos, table, strlen(table)); + pos+= stmt->dbc->mysql_proxy->real_escape_string(pos, table, strlen(table)); pos= myodbc_stpmov(pos, "`"); - MYLOG_QUERY(stmt, buff); + MYLOG_STMT_TRACE(stmt, buff); assert(stmt); LOCK_DBC(stmt->dbc); if (exec_stmt_query(stmt, buff, strlen(buff), FALSE) || - !(res= mysql_store_result(stmt->dbc->mysql))) + !(res = stmt->dbc->mysql_proxy->store_result())) { stmt->set_error(MYERR_S1000); return FALSE; } - while ((row= mysql_fetch_row(res)) && + while ((row = stmt->dbc->mysql_proxy->fetch_row(res)) && stmt->cursor.pk_count < MY_MAX_PK_PARTS) { int seq= atoi(row[3]); @@ -266,7 +269,7 @@ static my_bool check_if_usable_unique_key_exists(STMT *stmt) /* Forget about any key we had in progress, we didn't have it all. */ stmt->cursor.pk_count= seq_in_index= 0; } - mysql_free_result(res); + stmt->dbc->mysql_proxy->free_result(res); /* Remember that we've figured this out already. */ stmt->cursor.pk_validated= 1; @@ -447,7 +450,7 @@ static bool insert_field_std(STMT *stmt, MYSQL_RES *result, iprec_(DESC_PARAM, DESC_IMP); DESCREC *aprec= &aprec_, *iprec= &iprec_; - MYSQL_FIELD *field= mysql_fetch_field_direct(result,nSrcCol); + MYSQL_FIELD *field= stmt->dbc->mysql_proxy->fetch_field_direct(result,nSrcCol); MYSQL_ROW row_data; SQLLEN length; char as_string[50], *dummy; @@ -572,10 +575,10 @@ static SQLRETURN append_all_fields_std(STMT *stmt, std::string &str) */ select = "SELECT * FROM `" + stmt->table_name + "` LIMIT 0"; - MYLOG_QUERY(stmt, select.c_str()); - LOCK_DBC(stmt->dbc); + MYLOG_STMT_TRACE(stmt, select.c_str()); + LOCK_STMT(stmt); if (exec_stmt_query_std(stmt, select, false) || - !(presultAllColumns= mysql_store_result(stmt->dbc->mysql))) + !(presultAllColumns = stmt->dbc->mysql_proxy->store_result())) { stmt->set_error(MYERR_S1000); return SQL_ERROR; @@ -585,9 +588,10 @@ static SQLRETURN append_all_fields_std(STMT *stmt, std::string &str) If the number of fields in the underlying table is not the same as our result set, we bail out -- we need them all! */ - if (mysql_num_fields(presultAllColumns) != mysql_num_fields(result)) + if (stmt->dbc->mysql_proxy->num_fields(presultAllColumns) != + stmt->dbc->mysql_proxy->num_fields(result)) { - mysql_free_result(presultAllColumns); + stmt->dbc->mysql_proxy->free_result(presultAllColumns); return SQL_ERROR; } @@ -610,7 +614,7 @@ static SQLRETURN append_all_fields_std(STMT *stmt, std::string &str) { stmt->set_error(MYERR_S1000, "Invalid use of floating point comparision in positioned operations",0); - mysql_free_result(presultAllColumns); + stmt->dbc->mysql_proxy->free_result(presultAllColumns); return SQL_ERROR; } @@ -625,7 +629,7 @@ static SQLRETURN append_all_fields_std(STMT *stmt, std::string &str) str.append("="); if (insert_field_std(stmt, result, str, j)) { - mysql_free_result(presultAllColumns); + stmt->dbc->mysql_proxy->free_result(presultAllColumns); return SQL_ERROR; } found_field= TRUE; @@ -638,12 +642,12 @@ static SQLRETURN append_all_fields_std(STMT *stmt, std::string &str) */ if (!found_field) { - mysql_free_result(presultAllColumns); + stmt->dbc->mysql_proxy->free_result(presultAllColumns); return SQL_ERROR; } } - mysql_free_result(presultAllColumns); + stmt->dbc->mysql_proxy->free_result(presultAllColumns); return SQL_SUCCESS; } @@ -727,9 +731,8 @@ static SQLRETURN build_set_clause_std(STMT *stmt, SQLULEN irow, for ( ncol= 0; ncol < stmt->result->field_count; ++ncol ) { SQLLEN *pcbValue; - SQLCHAR *to = (SQLCHAR*)stmt->buf(); - field= mysql_fetch_field_direct(result,ncol); + field= stmt->dbc->mysql_proxy->fetch_field_direct(result,ncol); arrec= desc_get_rec(stmt->ard, ncol, FALSE); irrec= desc_get_rec(stmt->ird, ncol, FALSE); @@ -837,7 +840,7 @@ SQLRETURN my_pos_delete_std(STMT *stmt, STMT *stmtParam, nReturn= exec_stmt_query_std(stmt, str, false); if ( nReturn == SQL_SUCCESS || nReturn == SQL_SUCCESS_WITH_INFO ) { - stmtParam->affected_rows= mysql_affected_rows(stmt->dbc->mysql); + stmtParam->affected_rows= stmt->dbc->mysql_proxy->affected_rows(); nReturn= update_status(stmtParam,SQL_ROW_DELETED); } return nReturn; @@ -894,7 +897,7 @@ SQLRETURN my_pos_update_std( STMT * pStmtCursor, rc = my_SQLExecute( pStmtTemp ); if ( SQL_SUCCEEDED( rc ) ) { - pStmt->affected_rows = mysql_affected_rows( pStmtTemp->dbc->mysql ); + pStmt->affected_rows = pStmtTemp->dbc->mysql_proxy->affected_rows(); rc = update_status( pStmt, SQL_ROW_UPDATED ); } else if (rc == SQL_NEED_DATA) @@ -933,6 +936,10 @@ static SQLRETURN fetch_bookmark(STMT *stmt) IS_BOOKMARK_VARIABLE(stmt); arrec= desc_get_rec(stmt->ard, -1, FALSE); + if (arrec == NULL) + { + return SQL_ERROR; + } if (!ARD_IS_BOUND(arrec)) { @@ -1007,6 +1014,10 @@ static SQLRETURN setpos_delete_bookmark_std(STMT *stmt, std::string &query) IS_BOOKMARK_VARIABLE(stmt); arrec= desc_get_rec(stmt->ard, -1, FALSE); + if (arrec == NULL) + { + return SQL_ERROR; + } if (!ARD_IS_BOUND(arrec)) { @@ -1043,7 +1054,7 @@ static SQLRETURN setpos_delete_bookmark_std(STMT *stmt, std::string &query) /* execute our DELETE statement */ if (!(nReturn= exec_stmt_query_std(stmt, query, false))) { - affected_rows+= stmt->dbc->mysql->affected_rows; + affected_rows+= stmt->dbc->mysql_proxy->get_affected_rows(); } if (stmt->stmt_options.rowStatusPtr_ex) { @@ -1117,7 +1128,7 @@ static SQLRETURN setpos_delete_std(STMT *stmt, SQLUSMALLINT irow, /* execute our DELETE statement */ if (!(nReturn= exec_stmt_query_std(stmt, query, false))) { - affected_rows+= stmt->dbc->mysql->affected_rows; + affected_rows+= stmt->dbc->mysql_proxy->get_affected_rows(); } } while ( ++rowset_pos <= rowset_end ); @@ -1163,6 +1174,10 @@ static SQLRETURN setpos_update_bookmark_std(STMT *stmt, std::string &query) IS_BOOKMARK_VARIABLE(stmt); arrec= desc_get_rec(stmt->ard, -1, FALSE); + if (arrec == NULL) + { + return SQL_ERROR; + } if (!ARD_IS_BOUND(arrec)) { @@ -1208,7 +1223,7 @@ static SQLRETURN setpos_update_bookmark_std(STMT *stmt, std::string &query) if (!(nReturn= exec_stmt_query_std(stmt, query, false))) { - affected+= mysql_affected_rows(stmt->dbc->mysql); + affected+= stmt->dbc->mysql_proxy->affected_rows(); } if (stmt->stmt_options.rowStatusPtr_ex) { @@ -1291,7 +1306,7 @@ static SQLRETURN setpos_update_std(STMT *stmt, SQLUSMALLINT irow, if (!(nReturn= exec_stmt_query_std(stmt, query, false))) { - affected+= mysql_affected_rows(stmt->dbc->mysql); + affected+= stmt->dbc->mysql_proxy->affected_rows(); } else if (!SQL_SUCCEEDED(nReturn)) { @@ -1372,7 +1387,7 @@ static SQLRETURN batch_insert_std( STMT *stmt, SQLULEN irow, std::string &query query.append("("); for ( ncol= 0; ncol < result->field_count; ++ncol ) { - MYSQL_FIELD *field= mysql_fetch_field_direct(result, ncol); + MYSQL_FIELD *field= stmt->dbc->mysql_proxy->fetch_field_direct(result, ncol); DESCREC *arrec; SQLLEN ind_or_len= 0; @@ -1477,7 +1492,6 @@ static SQLRETURN batch_insert_std( STMT *stmt, SQLULEN irow, std::string &query if (stmt->stmt_options.bookmarks == SQL_UB_VARIABLE) { - ulong copy_bytes= 0; int _len= 0; char _value[21]; DESCREC *arrec; @@ -1764,9 +1778,9 @@ SQLRETURN SQL_API my_SQLSetPos(SQLHSTMT hstmt, SQLSETPOSIROW irow, /* build list of column names */ for (nCol= 0; nCol < result->field_count; ++nCol) { - MYSQL_FIELD *field= mysql_fetch_field_direct(result, nCol); + MYSQL_FIELD *field= stmt->dbc->mysql_proxy->fetch_field_direct(result, nCol); myodbc_append_quoted_name_std(ins_query, field->org_name); - if (nCol + 1 < result->field_count) + if (nCol < result->field_count - 1) ins_query.append(","); } diff --git a/driver/dll.cc b/driver/dll.cc index 132bc65d..0fb572a1 100644 --- a/driver/dll.cc +++ b/driver/dll.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -82,6 +84,10 @@ void myodbc_init(void) if (myodbc_inited > 1) return; + // This library_init call is causing the test my_data to crash on mac. + // TODO: Find alternate solution + // mysql_library_init(0, nullptr, nullptr); + if(!mysys_inited) { my_sys_init(); @@ -170,7 +176,7 @@ void dll_location_init(HMODULE inst) std::string dll_loc; dll_loc.reserve(buflen); - GetModuleFileNameA((HMODULE)inst, dll_loc.data(), buflen); + GetModuleFileNameA((HMODULE)inst, (LPSTR)dll_loc.data(), buflen); current_dll_location = dll_loc.data(); auto bs_pos = current_dll_location.find_last_of('\\'); if (bs_pos != std::string::npos) diff --git a/driver/driver.def.cmake b/driver/driver.def.cmake index 0f1097fe..c940b73b 100644 --- a/driver/driver.def.cmake +++ b/driver/driver.def.cmake @@ -1,3 +1,5 @@ +;// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +;// ;// Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. ;// ;// This program is free software; you can redistribute it and/or modify diff --git a/driver/driver.h b/driver/driver.h index 609e4fab..684a2fec 100644 --- a/driver/driver.h +++ b/driver/driver.h @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -34,10 +36,14 @@ #ifndef __DRIVER_H__ #define __DRIVER_H__ +#include + #include "../MYODBC_MYSQL.h" #include "../MYODBC_CONF.h" #include "../MYODBC_ODBC.h" -#include "installer.h" +#include "util/installer.h" +#include "failover.h" +#include "mysql_proxy.h" /* Disable _attribute__ on non-gcc compilers. */ #if !defined(__attribute__) && !defined(__GNUC__) @@ -111,15 +117,15 @@ #if defined(__APPLE__) -#define DRIVER_QUERY_LOGFILE "/tmp/myodbc.sql" +#define DRIVER_LOG_FILE "/tmp/myodbc.log" -#elif defined(_UNIX_) +#elif defined(__UNIX__) -#define DRIVER_QUERY_LOGFILE "/tmp/myodbc.sql" +#define DRIVER_LOG_FILE "/tmp/myodbc.log" #else -#define DRIVER_QUERY_LOGFILE "myodbc.sql" +#define DRIVER_LOG_FILE "myodbc.log" #endif @@ -237,7 +243,7 @@ extern std::mutex global_fido_mutex; #if defined _WIN32 - #define DECLARE_LOCALE_HANDLE int loc; + #define DECLARE_LOCALE_HANDLE int loc = 0; #define __LOCALE_SET(LOC) \ { \ @@ -289,6 +295,15 @@ extern std::mutex global_fido_mutex; DONT_USE_LOCALE_CHECK(STMT) \ __LOCALE_RESTORE() +struct Srv_host_detail { + std::string name; + unsigned int port = MYSQL_PORT; +}; + +std::vector parse_host_list(const char *hosts_str, + unsigned int default_port); + +std::shared_ptr get_host_info_from_ds(DataSource* ds); typedef struct { int perms; @@ -516,16 +531,16 @@ struct DESC { std::list stmt_list; - void stmt_list_remove(STMT *stmt) + void stmt_list_remove(STMT * p_stmt) { if (alloc_type == SQL_DESC_ALLOC_USER) - stmt_list.remove(stmt); + stmt_list.remove(p_stmt); } - void stmt_list_add(STMT *stmt) + void stmt_list_add(STMT *p_stmt) { if (alloc_type == SQL_DESC_ALLOC_USER) - stmt_list.emplace_back(stmt); + stmt_list.emplace_back(p_stmt); } inline bool is_apd() @@ -589,45 +604,46 @@ struct ENV }; +static std::atomic_ulong last_dbc_id{1}; + /* Connection handler */ struct DBC { - ENV *env; - MYSQL *mysql; + ENV *env; + MYSQL_PROXY *mysql_proxy; std::list stmt_list; std::list desc_list; // Explicit descriptors - STMT_OPTIONS stmt_options; - MYERROR error; - FILE *query_log = nullptr; - char st_error_prefix[255] = { 0 }; - std::string database; - SQLUINTEGER login_timeout = 0; - time_t last_query_time = 0; - int txn_isolation = 0; - uint port = 0; - uint cursor_count = 0; - ulong net_buffer_len = 0; - uint commit_flag = 0; - bool has_query_attrs = false; + STMT_OPTIONS stmt_options; + MYERROR error; + std::shared_ptr log_file; // empty shared_ptr + char st_error_prefix[255] = { 0 }; + std::string database; + SQLUINTEGER login_timeout = 0; + time_t last_query_time = 0; + int txn_isolation = 0; + uint port = 0; + uint cursor_count = 0; + ulong net_buffer_len = 0; + uint commit_flag = 0; + bool has_query_attrs = false; + ulong id; + std::recursive_mutex lock; - // Whether SQL*ConnectW was used - bool unicode = false; - // 'ANSI' charset (SQL_C_CHAR) - CHARSET_INFO *ansi_charset_info = nullptr, - // Connection charset ('ANSI' or utf-8) - *cxn_charset_info = nullptr; - MY_SYNTAX_MARKERS *syntax = nullptr; - // data source used to connect (parsed or stored) - DataSource *ds = nullptr; - // value of the sql_select_limit currently set for a session - // (SQLULEN)(-1) if wasn't set - SQLULEN sql_select_limit = -1; - // Connection have been put to the pool - int need_to_wakeup = 0; + bool unicode = false; // Whether SQL*ConnectW was used + CHARSET_INFO *ansi_charset_info = nullptr, // 'ANSI' charset (SQL_C_CHAR) + *cxn_charset_info = nullptr; // Connection charset ('ANSI' or utf-8) + MY_SYNTAX_MARKERS *syntax = nullptr; + DataSource *ds = nullptr; // data source used to connect (parsed or stored) + SQLULEN sql_select_limit = -1; // value of the sql_select_limit currently set for a session + // (SQLULEN)(-1) if wasn't set + int need_to_wakeup = 0; // Connection have been put to the pool + bool transaction_open = false; // Flag to indicate whether we have a transaction open fido_callback_func fido_callback = nullptr; + FAILOVER_HANDLER *fh = nullptr; /* Failover handler */ + DBC(ENV *p_env); void free_explicit_descriptors(); void free_connection_stmts(); @@ -635,15 +651,17 @@ struct DBC void remove_desc(DESC *desc); SQLRETURN set_error(char *state, const char *message, uint errcode); SQLRETURN set_error(char *state); - SQLRETURN connect(DataSource *ds); + SQLRETURN connect(DataSource *dsrc, bool failover_enabled); void execute_prep_stmt(MYSQL_STMT *pstmt, std::string &query, MYSQL_BIND *param_bind, MYSQL_BIND *result_bind); - inline bool transactions_supported() - { return mysql->server_capabilities & CLIENT_TRANSACTIONS; } + inline bool transactions_supported() { + return mysql_proxy->get_server_capabilities() & CLIENT_TRANSACTIONS; + } - inline bool autocommit_is_on() - { return mysql->server_status & SERVER_STATUS_AUTOCOMMIT; } + inline bool autocommit_is_on() { + return mysql_proxy->get_server_status() & SERVER_STATUS_AUTOCOMMIT; + } void close(); ~DBC(); @@ -765,7 +783,7 @@ struct ODBC_RESULTSET {} void reset(MYSQL_RES *r = nullptr) - { if (res) mysql_free_result(res); res = r; } + { if (res) mysql_free_result(res); res = r; } // TODO Replace with proxy call MYSQL_RES *release() { @@ -955,9 +973,9 @@ struct ODBC_STMT { MYSQL_STMT *m_stmt; - ODBC_STMT(MYSQL *mysql) { m_stmt = mysql_stmt_init(mysql); } + ODBC_STMT(MYSQL_PROXY *mysql_proxy) { m_stmt = mysql_proxy->stmt_init(); } operator MYSQL_STMT*() { return m_stmt; } - ~ODBC_STMT() { mysql_stmt_close(m_stmt); } + ~ODBC_STMT() { mysql_stmt_close(m_stmt); } // TODO Replace with proxy call }; @@ -1072,7 +1090,7 @@ struct STMT */ SQLRETURN set_error(const char *state); - STMT(DBC *d) : dbc(d), result(NULL), array(NULL), result_array(NULL), + STMT(DBC *d) : dbc(d), result(NULL), fake_result(FALSE), array(NULL), result_array(NULL), current_values(NULL), fields(NULL), end_of_set(NULL), tempbuf(), stmt_options(dbc->stmt_options), lengths(nullptr), affected_rows(0), @@ -1148,7 +1166,7 @@ namespace myodbc { SQLWSTRING string_connect_in; assert(params->driver && *params->driver); - ds_set_strattr(¶ms->name, NULL); + ds_set_wstrattr(¶ms->name, NULL); ds_to_kvpair(params, string_connect_in, ';'); if (SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc) != SQL_SUCCESS) { @@ -1232,7 +1250,8 @@ void delete_param_bind(DYNAMIC_ARRAY *param_bind); #include "myutil.h" -#include "stringutil.h" +#include "util/stringutil.h" +#include "mylog.h" SQLRETURN SQL_API MySQLColAttribute(SQLHSTMT hstmt, SQLUSMALLINT column, diff --git a/driver/driver.rc.cmake b/driver/driver.rc.cmake index 00d2b135..3bf0f248 100644 --- a/driver/driver.rc.cmake +++ b/driver/driver.rc.cmake @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify diff --git a/driver/error.cc b/driver/error.cc index 9ff258c2..d75cd939 100644 --- a/driver/error.cc +++ b/driver/error.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -99,6 +101,8 @@ static MYODBC3_ERR_STR myodbc3_errors[]= {"42S22","Column not found", SQL_ERROR}, {"08S01","Communication link failure", SQL_ERROR}, {"08004","Server rejected the connection", SQL_ERROR}, + {"08S02","Communication link changed", SQL_ERROR}, + {"08007","Connection failure during transaction", SQL_ERROR} }; @@ -277,9 +281,9 @@ SQLRETURN set_env_error(ENV *env, myodbc_errid errid, const char *errtext, */ SQLRETURN set_conn_error(DBC *dbc, myodbc_errid errid, const char *errtext, - SQLINTEGER errcode) + SQLINTEGER errcode, char* prefix) { - dbc->error = MYERROR(errid, errtext, errcode, MYODBC_ERROR_PREFIX); + dbc->error = MYERROR(errid, errtext, errcode, prefix); return dbc->error.retcode; } @@ -288,11 +292,11 @@ SQLRETURN set_conn_error(DBC *dbc, myodbc_errid errid, const char *errtext, @type : myodbc3 internal @purpose : sets a myodbc_malloc() failure on a MYSQL* connection */ -void set_mem_error(MYSQL *mysql) +void set_mem_error(MYSQL_PROXY* mysql_proxy) { - mysql->net.last_errno= CR_OUT_OF_MEMORY; - myodbc_stpmov(mysql->net.last_error, "Memory allocation failed"); - myodbc_stpmov(mysql->net.sqlstate, "HY001"); + mysql_proxy->set_last_error_code(CR_OUT_OF_MEMORY); + myodbc_stpmov(mysql_proxy->get_last_error(), "Memory allocation failed"); + myodbc_stpmov(mysql_proxy->get_sqlstate(), "HY001"); } @@ -303,7 +307,7 @@ void set_mem_error(MYSQL *mysql) */ SQLRETURN handle_connection_error(STMT *stmt) { - unsigned int err= mysql_errno(stmt->dbc->mysql); + unsigned int err= stmt->dbc->mysql_proxy->error_code(); switch (err) { case 0: /* no error */ return SQL_SUCCESS; @@ -312,13 +316,17 @@ SQLRETURN handle_connection_error(STMT *stmt) #if MYSQL_VERSION_ID > 80023 case ER_CLIENT_INTERACTION_TIMEOUT: #endif - return stmt->set_error("08S01", mysql_error(stmt->dbc->mysql), err); + const char *error_code, *error_msg; + if (stmt->dbc->fh->trigger_failover_if_needed("08S01", error_code, error_msg)) + return stmt->set_error(error_code, error_msg, 0); + else + return stmt->set_error("08S01", stmt->dbc->mysql_proxy->error(), err); case CR_OUT_OF_MEMORY: - return stmt->set_error("HY001", mysql_error(stmt->dbc->mysql), err); + return stmt->set_error("HY001", stmt->dbc->mysql_proxy->error(), err); case CR_COMMANDS_OUT_OF_SYNC: case CR_UNKNOWN_ERROR: default: - return stmt->set_error("HY000", mysql_error(stmt->dbc->mysql), err); + return stmt->set_error("HY000", stmt->dbc->mysql_proxy->error(), err); } } @@ -427,11 +435,11 @@ MySQLGetDiagRec(SQLSMALLINT handle_type, SQLHANDLE handle, SQLSMALLINT record, bool is_odbc3_subclass(std::string sqlstate) { char *states[]= { "01S00", "01S01", "01S02", "01S06", "01S07", "07S01", - "08S01", "21S01", "21S02", "25S01", "25S02", "25S03", "42S01", "42S02", - "42S11", "42S12", "42S21", "42S22", "HY095", "HY097", "HY098", "HY099", - "HY100", "HY101", "HY105", "HY107", "HY109", "HY110", "HY111", "HYT00", - "HYT01", "IM001", "IM002", "IM003", "IM004", "IM005", "IM006", "IM007", - "IM008", "IM010", "IM011", "IM012"}; + "08S01", "08S02", "08007", "21S01", "21S02", "25S01", "25S02", "25S03", + "42S01", "42S02", "42S11", "42S12", "42S21", "42S22", "HY095", "HY097", + "HY098", "HY099", "HY100", "HY101", "HY105", "HY107", "HY109", "HY110", + "HY111", "HYT00", "HYT01", "IM001", "IM002", "IM003", "IM004", "IM005", + "IM006", "IM007", "IM008", "IM010", "IM011", "IM012"}; size_t i; if (sqlstate.empty()) @@ -493,7 +501,7 @@ MySQLGetDiagField(SQLSMALLINT handle_type, SQLHANDLE handle, SQLSMALLINT record, if (!stmt->result) *(SQLLEN *)num_value= 0; else - *(SQLLEN *)num_value= (SQLLEN) mysql_num_rows(stmt->result); + *(SQLLEN *)num_value= (SQLLEN) dbc->mysql_proxy->num_rows(stmt->result); return SQL_SUCCESS; case SQL_DIAG_DYNAMIC_FUNCTION: @@ -544,7 +552,7 @@ MySQLGetDiagField(SQLSMALLINT handle_type, SQLHANDLE handle, SQLSMALLINT record, case SQL_DIAG_CONNECTION_NAME: { - DataSource *ds; + DataSource *ds = nullptr; if (record <= 0) return SQL_ERROR; @@ -580,7 +588,7 @@ MySQLGetDiagField(SQLSMALLINT handle_type, SQLHANDLE handle, SQLSMALLINT record, case SQL_DIAG_SERVER_NAME: { - DataSource *ds; + DataSource *ds = nullptr; if (record <= 0) return SQL_ERROR; if (handle_type == SQL_HANDLE_DESC) diff --git a/driver/error.h b/driver/error.h index 675da5fa..cddda57c 100644 --- a/driver/error.h +++ b/driver/error.h @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -40,6 +42,8 @@ #ifndef __ERROR_H__ #define __ERROR_H__ +#include "mysql_proxy.h" + /* Including driver version definitions */ #include "../MYODBC_CONF.h" /* @@ -125,6 +129,8 @@ typedef enum myodbc_errid MYERR_08S01, /* Please add new errors to the end of enum, and not in alphabet order */ MYERR_08004, + MYERR_08S02, + MYERR_08007, } myodbc_errid; /* @@ -191,9 +197,9 @@ struct MYERROR sqlstate.clear(); } - MYERROR(const char* state, MYSQL* mysql) : - MYERROR(state, mysql_error(mysql), - mysql_errno(mysql), MYODBC_ERROR_PREFIX) + MYERROR(const char* state, MYSQL_PROXY* mysql_proxy) : + MYERROR(state, mysql_proxy->error(), + mysql_proxy->error_code(), MYODBC_ERROR_PREFIX) {} MYERROR(const char* state, std::string errmsg) : diff --git a/driver/execute.cc b/driver/execute.cc old mode 100755 new mode 100644 index 6da5608e..b2927368 --- a/driver/execute.cc +++ b/driver/execute.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -32,6 +34,8 @@ */ #include "driver.h" +#include "driver/query_parsing.h" + #include @@ -40,10 +44,18 @@ @purpose : internal function to execute query and return result frees query if query != stmt->query */ -SQLRETURN do_query(STMT *stmt,char *query, SQLULEN query_length) +SQLRETURN do_query(STMT *stmt, char *query, SQLULEN query_length) { - int error= SQL_ERROR, native_error= 0; + if (stmt && stmt->dbc && stmt->dbc->fh) { + stmt->dbc->fh->invoke_start_time(); + } + assert(stmt); + + SQLRETURN error = SQL_ERROR; + int native_error = 0; + bool trigger_failover_upon_error = true; + LOCK_STMT_DEFER(stmt); if (!query) @@ -69,16 +81,17 @@ SQLRETURN do_query(STMT *stmt,char *query, SQLULEN query_length) query_length= strlen(query); } - MYLOG_QUERY(stmt, query); + MYLOG_STMT_TRACE(stmt, query); DO_LOCK_STMT(); - if ( check_if_server_is_alive( stmt->dbc ) ) + + if ( !is_server_alive( stmt->dbc ) ) { stmt->set_error("08S01" /* "HYT00" */, - mysql_error(stmt->dbc->mysql), - mysql_errno(stmt->dbc->mysql)); + stmt->dbc->mysql_proxy->error(), + stmt->dbc->mysql_proxy->error_code()); translate_error((char*)stmt->error.sqlstate.c_str(), MYERR_08S01 /* S1000 */, - mysql_errno(stmt->dbc->mysql)); + stmt->dbc->mysql_proxy->error_code()); goto exit; } @@ -105,10 +118,15 @@ SQLRETURN do_query(STMT *stmt,char *query, SQLULEN query_length) scroller_create(stmt, query, query_length); scroller_move(stmt); - MYLOG_QUERY(stmt, stmt->scroller.query); + MYLOG_STMT_TRACE(stmt, stmt->scroller.query); - native_error = mysql_real_query(stmt->dbc->mysql, stmt->scroller.query, - (unsigned long)stmt->scroller.query_len); + SQLRETURN rc = odbc_stmt(stmt->dbc, stmt->scroller.query, + static_cast(stmt->scroller.query_len), false); + if (!SQL_SUCCEEDED(rc)) + { + native_error = stmt->dbc->error.native_error; + trigger_failover_upon_error = false; // possible failover was already handled in odbc_stmt() + } } /* Not using ssps for scroller so far. Relaxing a bit condition if allow_multiple_statements option selected by primitive check if @@ -117,52 +135,87 @@ SQLRETURN do_query(STMT *stmt,char *query, SQLULEN query_length) { native_error = 0; if (stmt->param_bind.size() && stmt->param_count) - native_error = mysql_stmt_bind_param(stmt->ssps, &stmt->param_bind[0]); + native_error = stmt->dbc->mysql_proxy->stmt_bind_param(stmt->ssps, &stmt->param_bind[0]); if (native_error == 0) { - native_error= mysql_stmt_execute(stmt->ssps); + native_error = stmt->dbc->mysql_proxy->stmt_execute(stmt->ssps); } else { stmt->set_error("HY000", - mysql_stmt_error(stmt->ssps), - mysql_stmt_errno(stmt->ssps)); + stmt->dbc->mysql_proxy->stmt_error(stmt->ssps), + stmt->dbc->mysql_proxy->stmt_errno(stmt->ssps)); /* For some errors - translating to more appropriate status */ translate_error((char*)stmt->error.sqlstate.c_str(), MYERR_S1000, - mysql_stmt_errno(stmt->ssps)); + stmt->dbc->mysql_proxy->stmt_errno(stmt->ssps)); goto exit; } - MYLOG_QUERY(stmt, "ssps has been executed"); + MYLOG_STMT_TRACE(stmt, "ssps has been executed"); } else { - MYLOG_QUERY(stmt, "Using direct execution"); - /* Need to close ps handler if it is open as our relsult will be generated + MYLOG_STMT_TRACE(stmt, "Using direct execution"); + /* Need to close ps handler if it is open as our result will be generated by direct execution. and ps handler may create some chaos */ ssps_close(stmt); - native_error = stmt->bind_query_attrs(false); - if (native_error == SQL_ERROR) + if (stmt->bind_query_attrs(false) == SQL_ERROR) { error = SQL_ERROR; goto exit; } - native_error= mysql_real_query(stmt->dbc->mysql,query,query_length); + SQLRETURN rc = odbc_stmt(stmt->dbc, query, query_length, false); + if (SQL_SUCCEEDED(rc)) + { + const std::vector statements = parse_query_into_statements(query); + for (int i = statements.size() - 1; i >= 0; i--) + { + std::string statement = statements[i]; + for (auto& c : statement) c = toupper(c); + + if (statement == "START TRANSACTION" || statement == "BEGIN") + { + stmt->dbc->transaction_open = true; + break; + } + else if (statement == "COMMIT" || statement == "ROLLBACK") + { + stmt->dbc->transaction_open = false; + break; + } + } + } + else + { + native_error = stmt->dbc->error.native_error; + trigger_failover_upon_error = false; // possible failover was already handled in odbc_stmt() + } } - MYLOG_QUERY(stmt, "query has been executed"); + MYLOG_STMT_TRACE(stmt, "query has been executed"); if (native_error) { - MYLOG_QUERY(stmt, mysql_error(stmt->dbc->mysql)); - stmt->set_error("HY000"); + const auto error_code = stmt->dbc->mysql_proxy->error_code(); + if (error_code) + { + MYLOG_STMT_TRACE(stmt, stmt->dbc->mysql_proxy->error()); + stmt->set_error("HY000"); + + // For some errors - translating to more appropriate status + translate_error((char*)stmt->error.sqlstate.c_str(), MYERR_S1000, error_code); + } + else + { + MYLOG_STMT_TRACE(stmt, stmt->dbc->error.message.c_str()); + stmt->set_error(stmt->dbc->error.sqlstate.c_str(), + stmt->dbc->error.message.c_str(), + stmt->dbc->error.native_error); + } - /* For some errors - translating to more appropriate status */ - translate_error((char*)stmt->error.sqlstate.c_str(), MYERR_S1000, - mysql_errno(stmt->dbc->mysql)); goto exit; } @@ -211,6 +264,11 @@ SQLRETURN do_query(STMT *stmt,char *query, SQLULEN query_length) error= SQL_SUCCESS; exit: + if (trigger_failover_upon_error && error == SQL_ERROR) { + const char *error_code, *error_msg; + if (stmt->dbc->fh->trigger_failover_if_needed(stmt->error.sqlstate.c_str(), error_code, error_msg)) + stmt->set_error(error_code, error_msg, 0); + } if (query != GET_QUERY(&stmt->query)) { @@ -522,7 +580,7 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, if (has_utf8_maxlen4 && - !is_minimum_version(stmt->dbc->mysql->server_version, "5.5.3")) + !is_minimum_version(stmt->dbc->mysql_proxy->get_server_version(), "5.5.3")) { return stmt->set_error("HY000", "Server does not support 4-byte encoded " @@ -571,12 +629,12 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, case SQL_C_FLOAT: if ( iprec->concise_type != SQL_NUMERIC && iprec->concise_type != SQL_DECIMAL ) { - sprintf(buff, "%.17e", *((float*) *res)); + snprintf(buff, buff_max, "%.17e", *((float*) *res)); } else { /* We should perpare this data for string comparison */ - sprintf(buff, "%.15e", *((float*) *res)); + snprintf(buff, buff_max, "%.15e", *((float*) *res)); } delocalize_radix(buff); *length= strlen(*res= buff); @@ -584,12 +642,12 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, case SQL_C_DOUBLE: if ( iprec->concise_type != SQL_NUMERIC && iprec->concise_type != SQL_DECIMAL ) { - sprintf(buff,"%.17e",*((double*) *res)); + snprintf(buff, buff_max, "%.17e", *((double*) *res)); } else { /* We should perpare this data for string comparison */ - sprintf(buff,"%.15e",*((double*) *res)); + snprintf(buff, buff_max, "%.15e",*((double*) *res)); } delocalize_radix(buff); *length= strlen(*res= buff); @@ -601,11 +659,11 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, if (stmt->dbc->ds->min_date_to_zero && !date->year && (date->month == date->day == 1)) { - *length= sprintf(buff, "0000-00-00"); + *length= snprintf(buff, buff_max, "0000-00-00"); } else { - *length= sprintf(buff, "%04d-%02d-%02d", date->year, date->month, date->day); + *length= snprintf(buff, buff_max, "%04d-%02d-%02d", date->year, date->month, date->day); } *res= buff; break; @@ -620,8 +678,8 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, return stmt->set_error("22008", "Not a valid time value supplied", 0); } - *length= sprintf(buff, "%02d:%02d:%02d", - time->hour, time->minute, time->second); + *length = snprintf(buff, buff_max, "%02d:%02d:%02d", + time->hour, time->minute, time->second); *res= buff; break; } @@ -633,14 +691,14 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, if (stmt->dbc->ds->min_date_to_zero && !time->year && (time->month == time->day == 1)) { - *length= sprintf(buff, "0000-00-00 %02d:%02d:%02d", time->hour, - time->minute, time->second); + *length= snprintf(buff, buff_max, "0000-00-00 %02d:%02d:%02d", time->hour, + time->minute, time->second); } else { - *length= sprintf(buff, "%04d-%02d-%02d %02d:%02d:%02d", - time->year, time->month, time->day, - time->hour, time->minute, time->second); + *length= snprintf(buff, buff_max, "%04d-%02d-%02d %02d:%02d:%02d", + time->year, time->month, time->day, + time->hour, time->minute, time->second); } if (time->fraction) @@ -650,7 +708,7 @@ SQLRETURN convert_c_type2str(STMT *stmt, SQLSMALLINT ctype, DESCREC *iprec, /* Start cleaning from the end */ int tmp_pos= 9; - sprintf(tmp_buf, ".%09d", time->fraction); + snprintf(tmp_buf, buff_max - *length, ".%09d", time->fraction); /* ODBC specification defines nanoseconds granularity for @@ -758,7 +816,6 @@ inline bool is_ts_char(char c) const char *get_date_time_substr(const char *data, long &len) { const char* d_start = data; - long idx = 0; while(len && !is_ts_char(*d_start)) { --len; @@ -791,7 +848,7 @@ const char *get_date_time_substr(const char *data, long &len) SQLRETURN insert_param(STMT *stmt, MYSQL_BIND *bind, DESC* apd, DESCREC *aprec, DESCREC *iprec, SQLULEN row) { - long length; + long length = 0; char buff[128], *data= NULL; BOOL convert= FALSE, free_data= FALSE; DBC *dbc= stmt->dbc; @@ -1243,7 +1300,7 @@ SQLRETURN insert_param(STMT *stmt, MYSQL_BIND *bind, DESC* apd, goto memerror; } - size_t added = mysql_real_escape_string(dbc->mysql, stmt->endbuf(), data, length); + size_t added = dbc->mysql_proxy->real_escape_string(stmt->endbuf(), data, length); stmt->buf_add_pos(added); stmt->add_to_buffer("'", 1); } @@ -1694,7 +1751,7 @@ static SQLRETURN find_next_dae_param(STMT *stmt, SQLPOINTER *token) static SQLRETURN execute_dae(STMT *stmt) { - SQLRETURN rc; + SQLRETURN rc = SQL_ERROR; char *query; SQLULEN query_len = 0; @@ -1750,7 +1807,7 @@ static SQLRETURN find_next_out_stream(STMT *stmt, SQLPOINTER *token) else { /* Magical out params fetch */ - mysql_stmt_fetch(stmt->ssps); + stmt->dbc->mysql_proxy->stmt_fetch(stmt->ssps); stmt->out_params_state = OPS_PREFETCHED; return SQL_SUCCESS; @@ -1934,7 +1991,7 @@ SQLRETURN SQL_API SQLCancel(SQLHSTMT hstmt) { char buff[40]; /* buff is always big enough because max length of %lu is 15 */ - sprintf(buff, "KILL /*!50000 QUERY */ %lu", mysql_thread_id(dbc->mysql)); + snprintf(buff, sizeof(buff), "KILL /*!50000 QUERY */ %lu", dbc->mysql_proxy->thread_id()); if (mysql_real_query(second, buff, strlen(buff))) { mysql_close(second); diff --git a/driver/failover.h b/driver/failover.h new file mode 100644 index 00000000..0b19e86f --- /dev/null +++ b/driver/failover.h @@ -0,0 +1,323 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __FAILOVER_H__ +#define __FAILOVER_H__ + +#include "topology_service.h" +#include "mylog.h" + +#include +#include + +#ifdef __linux__ +typedef std::u16string sqlwchar_string; +#else +typedef std::wstring sqlwchar_string; +#endif + +sqlwchar_string to_sqlwchar_string(const std::string& src); + +struct DBC; +struct DataSource; +typedef short SQLRETURN; + +class FAILOVER_CONNECTION_HANDLER { + public: + FAILOVER_CONNECTION_HANDLER(DBC* dbc); + virtual ~FAILOVER_CONNECTION_HANDLER(); + + virtual SQLRETURN do_connect(DBC* dbc_ptr, DataSource* ds, bool failover_enabled); + virtual MYSQL_PROXY* connect(const std::shared_ptr& host_info); + void update_connection(MYSQL_PROXY* new_connection, const std::string& new_host_name); + + private: + DBC* dbc; + DBC* clone_dbc(DBC* source_dbc); +}; + +struct READER_FAILOVER_RESULT { + bool connected = false; + std::shared_ptr new_host; + MYSQL_PROXY* new_connection; + + READER_FAILOVER_RESULT() + : connected{false}, new_host{nullptr}, new_connection{nullptr} {} + + READER_FAILOVER_RESULT(bool connected, std::shared_ptr new_host, + MYSQL_PROXY* new_connection) + : connected{connected}, + new_host{new_host}, + new_connection{new_connection} {} +}; + +// FAILOVER_SYNC enables synchronization between threads +class FAILOVER_SYNC { + public: + FAILOVER_SYNC(int num_tasks); + void increment_task(); + void mark_as_complete(bool cancel_other_tasks); + void wait_and_complete(int milliseconds); + virtual bool is_completed(); + + private: + int num_tasks; + std::mutex mutex_; + std::condition_variable cv; +}; + +class FAILOVER_READER_HANDLER { + public: + FAILOVER_READER_HANDLER( + std::shared_ptr topology_service, + std::shared_ptr connection_handler, + int failover_timeout_ms, int failover_reader_connect_timeout, + unsigned long dbc_id, bool enable_logging = false); + + ~FAILOVER_READER_HANDLER(); + + READER_FAILOVER_RESULT failover( + std::shared_ptr topology_info); + + virtual READER_FAILOVER_RESULT get_reader_connection( + std::shared_ptr topology_info, + FAILOVER_SYNC& f_sync); + + std::vector> build_hosts_list( + const std::shared_ptr& topology_info, + bool contain_writers); + + READER_FAILOVER_RESULT get_connection_from_hosts( + std::vector> hosts_list, + FAILOVER_SYNC& global_sync); + + protected: + int reader_connect_timeout_ms = 30000; // 30 sec + int max_failover_timeout_ms = 60000; // 60 sec + + private: + std::shared_ptr topology_service; + std::shared_ptr connection_handler; + const int READER_CONNECT_INTERVAL_SEC = 1; // 1 sec + std::shared_ptr logger = nullptr; + unsigned long dbc_id = 0; +}; + +// This struct holds results of Writer Failover Process. +struct WRITER_FAILOVER_RESULT { + bool connected = false; + bool is_new_host = false; // True if process connected to a new host. False if + // process re-connected to the same host + std::shared_ptr new_topology; + MYSQL_PROXY* new_connection; + + WRITER_FAILOVER_RESULT() + : connected{false}, + is_new_host{false}, + new_topology{nullptr}, + new_connection{nullptr} {} + + WRITER_FAILOVER_RESULT(bool connected, bool is_new_host, + std::shared_ptr new_topology, + MYSQL_PROXY* new_connection) + : connected{connected}, + is_new_host{is_new_host}, + new_topology{new_topology}, + new_connection{new_connection} {} +}; + +class FAILOVER_WRITER_HANDLER { + public: + FAILOVER_WRITER_HANDLER( + std::shared_ptr topology_service, + std::shared_ptr reader_handler, + std::shared_ptr connection_handler, + int writer_failover_timeout_ms, int read_topology_interval_ms, + int reconnect_writer_interval_ms, unsigned long dbc_id, bool enable_logging = false); + ~FAILOVER_WRITER_HANDLER(); + WRITER_FAILOVER_RESULT failover( + std::shared_ptr current_topology); + + protected: + int read_topology_interval_ms = 5000; // 5 sec + int reconnect_writer_interval_ms = 5000; // 5 sec + int writer_failover_timeout_ms = 60000; // 60 sec + + private: + std::shared_ptr topology_service; + std::shared_ptr connection_handler; + std::shared_ptr reader_handler; + std::shared_ptr logger = nullptr; + unsigned long dbc_id = 0; +}; + +class FAILOVER_HANDLER { + public: + FAILOVER_HANDLER(DBC* dbc, DataSource* ds); + FAILOVER_HANDLER( + DBC* dbc, DataSource* ds, + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + std::shared_ptr metrics_container); + ~FAILOVER_HANDLER(); + SQLRETURN init_cluster_info(); + bool trigger_failover_if_needed(const char* error_code, const char*& new_error_code, const char*& error_msg); + bool is_failover_enabled(); + bool is_rds(); + bool is_rds_proxy(); + bool is_cluster_topology_available(); + void invoke_start_time(); + std::string cluster_id = DEFAULT_CLUSTER_ID; + + private: + DBC* dbc = nullptr; + DataSource* ds = nullptr; + std::shared_ptr topology_service; + std::shared_ptr failover_reader_handler; + std::shared_ptr failover_writer_handler; + std::shared_ptr current_topology; + std::shared_ptr current_host = nullptr; + std::shared_ptr connection_handler = nullptr; + bool m_is_cluster_topology_available = false; + bool m_is_multi_writer_cluster = false; + bool m_is_rds_proxy = false; + bool m_is_rds = false; + bool m_is_rds_custom_cluster = false; + bool initialized = false; + + bool is_dns_pattern_valid(std::string host); + bool is_rds_dns(std::string host); + bool is_rds_proxy_dns(std::string host); + bool is_rds_custom_cluster_dns(std::string host); + SQLRETURN create_connection_and_initialize_topology(); + SQLRETURN reconnect(bool failover_enabled); + std::string get_rds_cluster_host_url(std::string host); + std::string get_rds_instance_host_pattern(std::string host); + bool is_ipv4(std::string host); + bool is_ipv6(std::string host); + bool failover_to_reader(const char*& new_error_code, const char*& error_msg); + bool failover_to_writer(const char*& new_error_code, const char*& error_msg); + + void set_cluster_id(std::string host, int port); + void set_cluster_id(std::string cluster_id); + std::shared_ptr metrics_container; + std::chrono::steady_clock::time_point invoke_start_time_ms; + std::chrono::steady_clock::time_point failover_start_time_ms; +}; + +// ************************************************************************************************ +// These are failover utilities/helpers. Perhaps belong to a separate header +// file, but here for now +// + +class FAILOVER { + public: + FAILOVER(std::shared_ptr connection_handler, + std::shared_ptr topology_service, + unsigned long dbc_id, bool enable_logging = false); + virtual ~FAILOVER() = default; + bool is_writer_connected(); + + protected: + bool connect(const std::shared_ptr& host_info); + void sleep(int miliseconds); + void release_new_connection(); + std::shared_ptr connection_handler; + std::shared_ptr topology_service; + MYSQL_PROXY* new_connection; + std::shared_ptr logger = nullptr; + unsigned long dbc_id = 0; +}; + +class CONNECT_TO_READER_HANDLER : public FAILOVER { +public: + CONNECT_TO_READER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + unsigned long dbc_id, bool enable_logging = false); + ~CONNECT_TO_READER_HANDLER(); + + void operator()( + std::shared_ptr reader, + FAILOVER_SYNC& f_sync, + READER_FAILOVER_RESULT& result); +}; + +class RECONNECT_TO_WRITER_HANDLER : public FAILOVER { + public: + RECONNECT_TO_WRITER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + int connection_interval, unsigned long dbc_id, bool enable_logging = false); + ~RECONNECT_TO_WRITER_HANDLER(); + + void operator()( + std::shared_ptr original_writer, + FAILOVER_SYNC& f_sync, + WRITER_FAILOVER_RESULT& result); + + private: + int reconnect_interval_ms; + + bool is_current_host_writer( + const std::shared_ptr& original_writer, + const std::shared_ptr& latest_topology); +}; + +class WAIT_NEW_WRITER_HANDLER : public FAILOVER { + public: + WAIT_NEW_WRITER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + std::shared_ptr current_topology, + std::shared_ptr reader_handler, + int connection_interval, unsigned long dbc_id, bool enable_logging = false); + ~WAIT_NEW_WRITER_HANDLER(); + + void operator()( + std::shared_ptr original_writer, + FAILOVER_SYNC& f_sync, + WRITER_FAILOVER_RESULT& result); + + private: + // TODO - initialize in constructor and define constant for default value + int read_topology_interval_ms = 5000; + std::shared_ptr reader_handler; + std::shared_ptr current_topology; + MYSQL_PROXY* reader_connection = nullptr; // To retrieve latest topology + std::shared_ptr current_reader_host; + + void refresh_topology_and_connect_to_new_writer( + std::shared_ptr original_writer, FAILOVER_SYNC& f_sync); + void connect_to_reader(FAILOVER_SYNC& f_sync); + bool connect_to_writer(std::shared_ptr writer_candidate); + void clean_up_reader_connection(); +}; + +#endif /* __FAILOVER_H__ */ diff --git a/driver/failover_connection_handler.cc b/driver/failover_connection_handler.cc new file mode 100644 index 00000000..453ece44 --- /dev/null +++ b/driver/failover_connection_handler.cc @@ -0,0 +1,127 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +/** + @file failover_connection_handler.c + @brief Failover connection functions. +*/ + +#include "driver.h" +#include "failover.h" + +#include +#include + +#ifdef __linux__ + sqlwchar_string to_sqlwchar_string(const std::string& src) { + return std::wstring_convert< std::codecvt_utf8_utf16< char16_t >, + char16_t >{} + .from_bytes(src); + } +#else + sqlwchar_string to_sqlwchar_string(const std::string& src) { + return std::wstring_convert< std::codecvt_utf8< wchar_t >, wchar_t >{} + .from_bytes(src); + } +#endif + +FAILOVER_CONNECTION_HANDLER::FAILOVER_CONNECTION_HANDLER(DBC* dbc) : dbc{dbc} {} + +FAILOVER_CONNECTION_HANDLER::~FAILOVER_CONNECTION_HANDLER() {} + +SQLRETURN FAILOVER_CONNECTION_HANDLER::do_connect(DBC* dbc_ptr, DataSource* ds, bool failover_enabled) { + return dbc_ptr->connect(ds, failover_enabled); +} + +MYSQL_PROXY* FAILOVER_CONNECTION_HANDLER::connect(const std::shared_ptr& host_info) { + + if (dbc == nullptr || dbc->ds == nullptr || host_info == nullptr) { + return nullptr; + } + + const auto new_host = to_sqlwchar_string(host_info->get_host()); + + DBC* dbc_clone = clone_dbc(dbc); + ds_set_wstrnattr(&dbc_clone->ds->server, (SQLWCHAR*)new_host.c_str(), new_host.size()); + + MYSQL_PROXY* new_connection = nullptr; + CLEAR_DBC_ERROR(dbc_clone); + const SQLRETURN rc = do_connect(dbc_clone, dbc_clone->ds, true); + + if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { + new_connection = dbc_clone->mysql_proxy; + dbc_clone->mysql_proxy = nullptr; + dbc_clone->ds = nullptr; + } + + my_SQLFreeConnect(dbc_clone); + + return new_connection; +} + +void FAILOVER_CONNECTION_HANDLER::update_connection( + MYSQL_PROXY* new_connection, const std::string& new_host_name) { + + if (new_connection->is_connected()) { + dbc->close(); + dbc->mysql_proxy->set_connection(new_connection); + + CLEAR_DBC_ERROR(dbc); + + const sqlwchar_string new_host_name_wstr = to_sqlwchar_string(new_host_name); + + // Update original ds to reflect change in host/server. + ds_set_wstrnattr(&dbc->ds->server, (SQLWCHAR*)new_host_name_wstr.c_str(), new_host_name_wstr.size()); + ds_set_strnattr(&dbc->ds->server8, (SQLCHAR*)new_host_name.c_str(), new_host_name.size()); + } +} + +DBC* FAILOVER_CONNECTION_HANDLER::clone_dbc(DBC* source_dbc) { + + DBC* dbc_clone = nullptr; + + SQLRETURN status = SQL_ERROR; + if (source_dbc && source_dbc->env) { + SQLHDBC hdbc; + SQLHENV henv = static_cast(source_dbc->env); + + status = my_SQLAllocConnect(henv, &hdbc); + if (status == SQL_SUCCESS || status == SQL_SUCCESS_WITH_INFO) { + dbc_clone = static_cast(hdbc); + dbc_clone->ds = ds_new(); + ds_copy(dbc_clone->ds, source_dbc->ds); + dbc_clone->mysql_proxy = new MYSQL_PROXY(dbc_clone, dbc_clone->ds); + } else { + const char* err = "Cannot allocate connection handle when cloning DBC in writer failover process"; + MYLOG_DBC_TRACE(dbc, err); + throw std::runtime_error(err); + } + } + return dbc_clone; +} diff --git a/driver/failover_handler.cc b/driver/failover_handler.cc new file mode 100644 index 00000000..73b7b2a7 --- /dev/null +++ b/driver/failover_handler.cc @@ -0,0 +1,522 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +/** + @file failover_handler.c + @brief Failover functions. +*/ + +#include +#include + +#include "driver.h" + +namespace { +const std::regex AURORA_DNS_PATTERN( + R"#((.+)\.(proxy-|cluster-|cluster-ro-|cluster-custom-)?([a-zA-Z0-9]+\.[a-zA-Z0-9\-]+\.rds\.amazonaws\.com))#", + std::regex_constants::icase); +const std::regex AURORA_PROXY_DNS_PATTERN( + R"#((.+)\.(proxy-[a-zA-Z0-9]+\.[a-zA-Z0-9\-]+\.rds\.amazonaws\.com))#", + std::regex_constants::icase); +const std::regex AURORA_CUSTOM_CLUSTER_PATTERN( + R"#((.+)\.(cluster-custom-[a-zA-Z0-9]+\.[a-zA-Z0-9\-]+\.rds\.amazonaws\.com))#", + std::regex_constants::icase); +const std::regex IPV4_PATTERN( + R"#(^(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){1}(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){2}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$)#"); +const std::regex IPV6_PATTERN(R"#(^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$)#"); +const std::regex IPV6_COMPRESSED_PATTERN( + R"#(^(([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})?)$)#"); +} // namespace + +FAILOVER_HANDLER::FAILOVER_HANDLER(DBC* dbc, DataSource* ds) + : FAILOVER_HANDLER( + dbc, ds, std::make_shared(dbc), + std::make_shared(dbc ? dbc->id : 0, ds ? ds->save_queries : false), + std::make_shared(dbc, ds)) {} + +FAILOVER_HANDLER::FAILOVER_HANDLER(DBC* dbc, DataSource* ds, + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + std::shared_ptr metrics_container) { + if (!dbc) { + throw std::runtime_error("DBC cannot be null."); + } + + if (!ds) { + throw std::runtime_error("DataSource cannot be null."); + } + + this->dbc = dbc; + this->ds = ds; + this->topology_service = topology_service; + this->topology_service->set_refresh_rate(ds->topology_refresh_rate); + this->topology_service->set_gather_metric(ds->gather_perf_metrics); + this->connection_handler = connection_handler; + + this->failover_reader_handler = std::make_shared( + this->topology_service, this->connection_handler, ds->failover_timeout, + ds->failover_reader_connect_timeout, dbc->id, ds->save_queries); + this->failover_writer_handler = std::make_shared( + this->topology_service, this->failover_reader_handler, + this->connection_handler, ds->failover_timeout, + ds->failover_topology_refresh_rate, + ds->failover_writer_reconnect_interval, dbc->id, ds->save_queries); + this->metrics_container = metrics_container; +} + +FAILOVER_HANDLER::~FAILOVER_HANDLER() {} + +SQLRETURN FAILOVER_HANDLER::init_cluster_info() { + SQLRETURN rc = SQL_ERROR; + if (initialized) { + return rc; + } + + if (!ds->enable_cluster_failover) { + // Use a standard default connection - no further initialization required + rc = connection_handler->do_connect(dbc, ds, false); + initialized = true; + return rc; + } + std::stringstream err; + // Cluster-aware failover is enabled + + this->current_host = get_host_info_from_ds(ds); + std::string main_host = this->current_host->get_host(); + unsigned int main_port = this->current_host->get_port(); + + const char* hp = + ds_get_utf8attr(ds->host_pattern, &ds->host_pattern8); + std::string hp_str(hp ? hp : ""); + + const char* clid = + ds_get_utf8attr(ds->cluster_id, &ds->cluster_id8); + std::string clid_str(clid ? clid : ""); + + if (!hp_str.empty()) { + unsigned int port = ds->port ? ds->port : MYSQL_PORT; + std::vector host_patterns; + + try { + host_patterns = parse_host_list(hp_str.c_str(), port); + } catch (std::string&) { + err << "Invalid host pattern: '" << hp_str << "' - the value could not be parsed"; + MYLOG_TRACE(dbc->log_file.get(), dbc->id, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + if (host_patterns.size() == 0) { + err << "Empty host pattern."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + std::string host_pattern(host_patterns[0].name); + unsigned int host_pattern_port = host_patterns[0].port; + + if (!is_dns_pattern_valid(host_pattern)) { + err << "Invalid host pattern: '" << host_pattern + << "' - the host pattern must contain a '?' character as a " + "placeholder for the DB instance identifiers of the cluster " + "instances"; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + auto host_template = std::make_shared(host_pattern, host_pattern_port); + topology_service->set_cluster_instance_template(host_template); + + m_is_rds = is_rds_dns(host_pattern); + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] m_is_rds=%s", m_is_rds ? "true" : "false"); + m_is_rds_proxy = is_rds_proxy_dns(host_pattern); + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] m_is_rds_proxy=%s", m_is_rds_proxy ? "true" : "false"); + m_is_rds_custom_cluster = is_rds_custom_cluster_dns(host_pattern); + + if (m_is_rds_proxy) { + err << "RDS Proxy url can't be used as an instance pattern."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + if (m_is_rds_custom_cluster) { + err << "RDS Custom Cluster endpoint can't be used as an instance pattern."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + if (!clid_str.empty()) { + set_cluster_id(clid_str); + + } else if (m_is_rds) { + // If it's a cluster endpoint, or a reader cluster endpoint, then + // let's use as cluster identification + std::string cluster_rds_host = + get_rds_cluster_host_url(host_pattern); + if (!cluster_rds_host.empty()) { + set_cluster_id(cluster_rds_host, host_pattern_port); + } + } + + rc = create_connection_and_initialize_topology(); + } else if (is_ipv4(main_host) || is_ipv6(main_host)) { + // TODO: do we need to setup host template in this case? + // HOST_INFO* host_template = new HOST_INFO(); + // host_template->host.assign(main_host); + // host_template->port = main_port; + // ts->setClusterInstanceTemplate(host_template); + + if (!clid_str.empty()) { + set_cluster_id(clid_str); + } + + rc = create_connection_and_initialize_topology(); + + if (m_is_cluster_topology_available) { + err << "Host Pattern configuration setting is required when IP " + "address is used to connect to a cluster that provides topology " + "information. If you would instead like to connect without " + "failover functionality, set the 'Disable Cluster Failover' " + "configuration property to true."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + m_is_rds = false; // actually we don't know + m_is_rds_proxy = false; // actually we don't know + + } else { + m_is_rds = is_rds_dns(main_host); + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] m_is_rds=%s", m_is_rds ? "true" : "false"); + m_is_rds_proxy = is_rds_proxy_dns(main_host); + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] m_is_rds_proxy=%s", m_is_rds_proxy ? "true" : "false"); + + if (!m_is_rds) { + // it's not RDS, maybe custom domain (CNAME) + auto host_template = + std::make_shared(main_host, main_port); + topology_service->set_cluster_instance_template(host_template); + + if (!clid_str.empty()) { + set_cluster_id(clid_str); + } + + rc = create_connection_and_initialize_topology(); + + if (m_is_cluster_topology_available) { + err << "The provided host appears to be a custom domain. The " + "driver requires the Host Pattern configuration setting " + "to be set for custom domains. If you would instead like " + "to connect without failover functionality, set the " + "'Disable Cluster Failover' configuration property to true."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + } else { + // It's RDS + + std::string rds_instance_host = get_rds_instance_host_pattern(main_host); + if (!rds_instance_host.empty()) { + topology_service->set_cluster_instance_template( + std::make_shared(rds_instance_host, main_port)); + } else { + err << "The provided host does not appear to match an expected " + "Aurora DNS pattern. Please set the Host Pattern " + "configuration to specify the host pattern for the " + "cluster you are trying to connect to."; + MYLOG_DBC_TRACE(dbc, err.str().c_str()); + throw std::runtime_error(err.str()); + } + + if (!clid_str.empty()) { + set_cluster_id(clid_str); + } else if (m_is_rds_proxy) { + // Each proxy is associated with a single cluster so it's safe + // to use RDS Proxy Url as cluster identification + set_cluster_id(main_host, main_port); + } else { + // If it's cluster endpoint or reader cluster endpoint, + // then let's use as cluster identification + + std::string cluster_rds_host = get_rds_cluster_host_url(main_host); + if (!cluster_rds_host.empty()) { + set_cluster_id(cluster_rds_host, main_port); + } else { + // Main host is an instance endpoint + set_cluster_id(main_host, main_port); + } + } + + rc = create_connection_and_initialize_topology(); + } + } + + initialized = true; + return rc; +} + +void FAILOVER_HANDLER::set_cluster_id(std::string host, int port) { + set_cluster_id(host + ":" + std::to_string(port)); +} + +void FAILOVER_HANDLER::set_cluster_id(std::string cid) { + this->cluster_id = cid; + topology_service->set_cluster_id(this->cluster_id); + metrics_container->set_cluster_id(this->cluster_id); +} + +bool FAILOVER_HANDLER::is_dns_pattern_valid(std::string host) { + return (host.find("?") != std::string::npos); +} + +bool FAILOVER_HANDLER::is_rds_dns(std::string host) { + return std::regex_match(host, AURORA_DNS_PATTERN); +} + +bool FAILOVER_HANDLER::is_rds_proxy_dns(std::string host) { + return std::regex_match(host, AURORA_PROXY_DNS_PATTERN); +} + +bool FAILOVER_HANDLER::is_rds_custom_cluster_dns(std::string host) { + return std::regex_match(host, AURORA_CUSTOM_CLUSTER_PATTERN); +} + +#if defined(__APPLE__) || defined(__linux__) + #define strcmp_case_insensitive(str1, str2) strcasecmp(str1, str2) +#else + #define strcmp_case_insensitive(str1, str2) strcmpi(str1, str2) +#endif + +std::string FAILOVER_HANDLER::get_rds_cluster_host_url(std::string host) { + std::smatch m; + if (std::regex_search(host, m, AURORA_DNS_PATTERN) && m.size() > 1) { + std::string gr1 = m.size() > 1 ? m.str(1) : std::string(""); + std::string gr2 = m.size() > 2 ? m.str(2) : std::string(""); + std::string gr3 = m.size() > 3 ? m.str(3) : std::string(""); + if (!gr1.empty() && !gr3.empty() && + (strcmp_case_insensitive(gr2.c_str(), "cluster-") == 0 || strcmp_case_insensitive(gr2.c_str(), "cluster-ro-") == 0)) { + std::string result; + result.assign(gr1); + result.append(".cluster-"); + result.append(gr3); + + return result; + } + } + return ""; +} + +std::string FAILOVER_HANDLER::get_rds_instance_host_pattern(std::string host) { + std::smatch m; + if (std::regex_search(host, m, AURORA_DNS_PATTERN) && m.size() > 3) { + if (!m.str(3).empty()) { + std::string result("?."); + result.append(m.str(3)); + + return result; + } + } + return ""; +} + +bool FAILOVER_HANDLER::is_failover_enabled() { + return (dbc != nullptr && ds != nullptr && + ds->enable_cluster_failover && + m_is_cluster_topology_available && + !m_is_rds_proxy && + !m_is_multi_writer_cluster); +} + +bool FAILOVER_HANDLER::is_rds() { return m_is_rds; } + +bool FAILOVER_HANDLER::is_rds_proxy() { return m_is_rds_proxy; } + +bool FAILOVER_HANDLER::is_cluster_topology_available() { + return m_is_cluster_topology_available; +} + +SQLRETURN FAILOVER_HANDLER::create_connection_and_initialize_topology() { + SQLRETURN rc = connection_handler->do_connect(dbc, ds, false); + if (!SQL_SUCCEEDED(rc)) { + metrics_container->register_invalid_initial_connection(true); + return rc; + } + + metrics_container->register_invalid_initial_connection(false); + current_topology = topology_service->get_topology(dbc->mysql_proxy, false); + if (current_topology) { + m_is_multi_writer_cluster = current_topology->is_multi_writer_cluster; + m_is_cluster_topology_available = current_topology->total_hosts() > 0; + MYLOG_DBC_TRACE(dbc, + "[FAILOVER_HANDLER] m_is_cluster_topology_available=%s", + m_is_cluster_topology_available ? "true" : "false"); + + // Since we can't determine whether failover should be enabled + // before we connect, there is a possibility we need to reconnect + // again with the correct connection settings for failover. + const unsigned int connect_timeout = get_connect_timeout(ds->connect_timeout); + const unsigned int network_timeout = get_network_timeout(ds->network_timeout); + + if (is_failover_enabled() && (connect_timeout != dbc->login_timeout || + network_timeout != ds->read_timeout || + network_timeout != ds->write_timeout)) { + rc = reconnect(true); + } + } + + return rc; +} + +SQLRETURN FAILOVER_HANDLER::reconnect(bool failover_enabled) { + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) { + dbc->close(); + } + return connection_handler->do_connect(dbc, ds, failover_enabled); +} + +bool FAILOVER_HANDLER::is_ipv4(std::string host) { + return std::regex_match(host, IPV4_PATTERN); +} + +bool FAILOVER_HANDLER::is_ipv6(std::string host) { + return std::regex_match(host, IPV6_PATTERN) || + std::regex_match(host, IPV6_COMPRESSED_PATTERN); +} + +// return true if failover is triggered, false if not triggered +bool FAILOVER_HANDLER::trigger_failover_if_needed(const char* error_code, + const char*& new_error_code, + const char*& error_msg) { + new_error_code = error_code; + std::string ec(error_code ? error_code : ""); + + if (!is_failover_enabled() || ec.empty()) { + return false; + } + + bool failover_success = false; // If failover happened & succeeded + bool in_transaction = !autocommit_on(dbc) || dbc->transaction_open; + + if (ec.rfind("08", 0) == 0) { // start with "08" + + // disable failure detection during failover + auto failure_detection_old_state = ds->enable_failure_detection; + ds->enable_failure_detection = false; + + // invalidate current connection + current_host = nullptr; + // close transaction if needed + + long long elasped_time_ms = + std::chrono::duration_cast(std::chrono::steady_clock::now() - invoke_start_time_ms).count(); + metrics_container->register_failure_detection_time(elasped_time_ms); + + failover_start_time_ms = std::chrono::steady_clock::now(); + + if (current_topology && current_topology->total_hosts() > 1 && + ds->allow_reader_connections) { // there are readers in topology + failover_success = failover_to_reader(new_error_code, error_msg); + elasped_time_ms = + std::chrono::duration_cast(std::chrono::steady_clock::now() - failover_start_time_ms).count(); + metrics_container->register_reader_failover_procedure_time(elasped_time_ms); + } else { + failover_success = failover_to_writer(new_error_code, error_msg); + elasped_time_ms = + std::chrono::duration_cast(std::chrono::steady_clock::now() - failover_start_time_ms).count(); + metrics_container->register_writer_failover_procedure_time(elasped_time_ms); + + } + metrics_container->register_failover_connects(failover_success); + + if (failover_success && in_transaction) { + new_error_code = "08007"; + error_msg = "Connection failure during transaction."; + } + + ds->enable_failure_detection = failure_detection_old_state; + + return true; + } + + return false; +} + +bool FAILOVER_HANDLER::failover_to_reader(const char*& new_error_code, const char*& error_msg) { + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] Starting reader failover procedure."); + auto result = failover_reader_handler->failover(current_topology); + + if (result.connected) { + current_host = result.new_host; + connection_handler->update_connection(result.new_connection, current_host->get_host()); + new_error_code = "08S02"; + error_msg = "The active SQL connection has changed."; + MYLOG_DBC_TRACE(dbc, + "[FAILOVER_HANDLER] The active SQL connection has changed " + "due to a connection failure. Please re-configure session " + "state if required."); + return true; + } else { + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] Unable to establish SQL connection to reader node."); + new_error_code = "08S01"; + error_msg = "The active SQL connection was lost."; + return false; + } + return false; +} + +bool FAILOVER_HANDLER::failover_to_writer(const char*& new_error_code, const char*& error_msg) { + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] Starting writer failover procedure."); + auto result = failover_writer_handler->failover(current_topology); + + if (!result.connected) { + MYLOG_DBC_TRACE(dbc, "[FAILOVER_HANDLER] Unable to establish SQL connection to writer node."); + new_error_code = "08S01"; + error_msg = "The active SQL connection was lost."; + return false; + } + if (result.is_new_host) { + // connected to a new writer host; take it over + current_topology = result.new_topology; + current_host = current_topology->get_writer(); + } + + connection_handler->update_connection( + result.new_connection, result.new_topology->get_writer()->get_host()); + + new_error_code = "08S02"; + error_msg = "The active SQL connection has changed."; + MYLOG_DBC_TRACE( + dbc, + "[FAILOVER_HANDLER] The active SQL connection has changed due to a " + "connection failure. Please re-configure session state if required."); + return true; +} + +void FAILOVER_HANDLER::invoke_start_time() { + invoke_start_time_ms = std::chrono::steady_clock::now(); +} diff --git a/driver/failover_reader_handler.cc b/driver/failover_reader_handler.cc new file mode 100644 index 00000000..43f1d9b3 --- /dev/null +++ b/driver/failover_reader_handler.cc @@ -0,0 +1,271 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "failover.h" + +#include +#include +#include +#include +#include +#include + +FAILOVER_READER_HANDLER::FAILOVER_READER_HANDLER( + std::shared_ptr topology_service, + std::shared_ptr connection_handler, + int failover_timeout_ms, int failover_reader_connect_timeout, + unsigned long dbc_id, bool enable_logging) + : topology_service{topology_service}, + connection_handler{connection_handler}, + max_failover_timeout_ms{failover_timeout_ms}, + reader_connect_timeout_ms{failover_reader_connect_timeout}, + dbc_id{dbc_id} { + + if (enable_logging) + logger = init_log_file(); +} + +FAILOVER_READER_HANDLER::~FAILOVER_READER_HANDLER() {} + +// Function called to start the Reader Failover process. +// This process will generate a list of available hosts: First readers that are up, then readers marked as down, then writers. +// If it goes through the list and does not succeed to connect, it tries again, endlessly. +READER_FAILOVER_RESULT FAILOVER_READER_HANDLER::failover( + std::shared_ptr current_topology) { + + READER_FAILOVER_RESULT reader_result(false, nullptr, nullptr); + if (!current_topology || current_topology->total_hosts() == 0) { + return reader_result; + } + + FAILOVER_SYNC global_sync(1); + + auto reader_result_future = std::async(std::launch::async, [=, &global_sync, &reader_result]() { + std::vector> hosts_list; + while (!global_sync.is_completed()) { + hosts_list = build_hosts_list(current_topology, true); + reader_result = get_connection_from_hosts(hosts_list, global_sync); + if (reader_result.connected) { + global_sync.mark_as_complete(true); + return; + } + // TODO Think of changes to the strategy if it went + // through all the hosts and did not connect. + std::this_thread::sleep_for(std::chrono::seconds(READER_CONNECT_INTERVAL_SEC)); + } + }); + + global_sync.wait_and_complete(max_failover_timeout_ms); + + if (reader_result_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + reader_result_future.get(); + } + + return reader_result; +} + +// Function to connect to a reader host. Often used to query/update the topology. +// If it goes through the list of readers and fails to connect, it tries again, endlessly. +// This function only tries to connect to reader hosts. +READER_FAILOVER_RESULT FAILOVER_READER_HANDLER::get_reader_connection( + std::shared_ptr topology_info, + FAILOVER_SYNC& f_sync) { + + // We build a list of all readers, up then down, without writers. + auto hosts = build_hosts_list(topology_info, false); + + while (!f_sync.is_completed()) { + auto reader_result = get_connection_from_hosts(hosts, f_sync); + // TODO Think of changes to the strategy if it went through all the readers and did not connect. + if (reader_result.connected) { + return reader_result; + } + } + // Return a false result if the connection request has been cancelled. + return READER_FAILOVER_RESULT(false, nullptr, nullptr); +} + +// Function that reads the topology and builds a list of hosts to connect to, in order of priority. +// boolean include_writers indicate whether one wants to append the writers to the end of the list or not. +std::vector> FAILOVER_READER_HANDLER::build_hosts_list( + const std::shared_ptr& topology_info, + bool include_writers) { + + std::vector> hosts_list; + + // We split reader hosts that are marked up from those marked down. + std::vector> readers_up; + std::vector> readers_down; + + auto readers = topology_info->get_readers(); + + for (auto reader : readers) { + if (reader->is_host_down()) { + readers_down.push_back(reader); + } else { + readers_up.push_back(reader); + } + } + + // Both lists of readers up and down are shuffled. + auto rng = std::default_random_engine{}; + std::shuffle(std::begin(readers_up), std::end(readers_up), rng); + std::shuffle(std::begin(readers_down), std::end(readers_down), rng); + + // Readers that are marked up go first, readers marked down go after. + hosts_list.insert(hosts_list.end(), readers_up.begin(), readers_up.end()); + hosts_list.insert(hosts_list.end(), readers_down.begin(), readers_down.end()); + + if (include_writers) { + auto writers = topology_info->get_writers(); + std::shuffle(std::begin(writers), std::end(writers), rng); + hosts_list.insert(hosts_list.end(), writers.begin(), writers.end()); + } + + return hosts_list; +} + +READER_FAILOVER_RESULT FAILOVER_READER_HANDLER::get_connection_from_hosts( + std::vector> hosts_list, FAILOVER_SYNC& global_sync) { + + size_t total_hosts = hosts_list.size(); + size_t i = 0; + + // This loop should end once it reaches the end of the list without a successful connection. + // The function calling it already has a neverending loop looking for a connection. + // Ending this loop will allow the calling function to update the list or change strategy if this failed. + while (!global_sync.is_completed() && i < total_hosts) { + // This boolean verifies if the next host in the list is also the last, meaning there's no host for the second thread. + bool odd_hosts_number = (i + 1 == total_hosts); + + FAILOVER_SYNC local_sync(1); + if (!odd_hosts_number) { + local_sync.increment_task(); + } + + CONNECT_TO_READER_HANDLER first_connection_handler(connection_handler, topology_service, dbc_id, logger != nullptr); + std::future first_connection_future; + READER_FAILOVER_RESULT first_connection_result(false, nullptr, nullptr); + + CONNECT_TO_READER_HANDLER second_connection_handler(connection_handler, topology_service, dbc_id, logger != nullptr); + std::future second_connection_future; + READER_FAILOVER_RESULT second_connection_result(false, nullptr, nullptr); + + std::shared_ptr first_reader_host = hosts_list.at(i); + first_connection_future = std::async(std::launch::async, std::ref(first_connection_handler), + std::ref(first_reader_host), std::ref(local_sync), + std::ref(first_connection_result)); + + if (!odd_hosts_number) { + std::shared_ptr second_reader_host = hosts_list.at(i + 1); + second_connection_future = std::async(std::launch::async, std::ref(second_connection_handler), + std::ref(second_reader_host), std::ref(local_sync), + std::ref(second_connection_result)); + } + + local_sync.wait_and_complete(reader_connect_timeout_ms); + + if (first_connection_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + first_connection_future.get(); + } + if (!odd_hosts_number && + second_connection_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + + second_connection_future.get(); + } + + if (first_connection_result.connected) { + MYLOG_TRACE(logger.get(), dbc_id, + "[FAILOVER_READER_HANDLER] Connected to reader: %s", + first_connection_result.new_host->get_host_port_pair().c_str()); + return first_connection_result; + } else if (!odd_hosts_number && second_connection_result.connected) { + MYLOG_TRACE(logger.get(), dbc_id, + "[FAILOVER_READER_HANDLER] Connected to reader: %s", + second_connection_result.new_host->get_host_port_pair().c_str()); + return second_connection_result; + } + // None has connected. We move on and try new hosts. + i += 2; + std::this_thread::sleep_for(std::chrono::seconds(READER_CONNECT_INTERVAL_SEC)); + } + + // The operation was either cancelled either reached the end of the list without connecting. + return READER_FAILOVER_RESULT(false, nullptr, nullptr); +} + +// *** CONNECT_TO_READER_HANDLER +// Handler to connect to a reader host. +CONNECT_TO_READER_HANDLER::CONNECT_TO_READER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + unsigned long dbc_id, bool enable_logging) + : FAILOVER{connection_handler, topology_service, dbc_id, enable_logging} {} + +CONNECT_TO_READER_HANDLER::~CONNECT_TO_READER_HANDLER() {} + +void CONNECT_TO_READER_HANDLER::operator()( + std::shared_ptr reader, + FAILOVER_SYNC& f_sync, + READER_FAILOVER_RESULT& result) { + + if (reader && !f_sync.is_completed()) { + + MYLOG_TRACE(logger.get(), dbc_id, + "[CONNECT_TO_READER_HANDLER] Trying to connect to reader: %s", + reader->get_host_port_pair().c_str()); + + if (connect(reader)) { + topology_service->mark_host_up(reader); + if (f_sync.is_completed()) { + // If another thread finishes first, or both timeout, this thread is canceled. + release_new_connection(); + } else { + result = READER_FAILOVER_RESULT(true, reader, std::move(this->new_connection)); + f_sync.mark_as_complete(true); + MYLOG_TRACE( + logger.get(), dbc_id, + "[CONNECT_TO_READER_HANDLER] Connected to reader: %s", + reader->get_host_port_pair().c_str()); + return; + } + } else { + topology_service->mark_host_down(reader); + MYLOG_TRACE( + logger.get(), dbc_id, + "[CONNECT_TO_READER_HANDLER] Failed to connect to reader: %s", + reader->get_host_port_pair().c_str()); + if (!f_sync.is_completed()) { + f_sync.mark_as_complete(false); + } + } + } + + release_new_connection(); +} diff --git a/driver/failover_writer_handler.cc b/driver/failover_writer_handler.cc new file mode 100644 index 00000000..da27dfc3 --- /dev/null +++ b/driver/failover_writer_handler.cc @@ -0,0 +1,353 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "failover.h" + +#include +#include +#include + +// **** FAILOVER_SYNC *************************************** +// used for thread synchronization +FAILOVER_SYNC::FAILOVER_SYNC(int num_tasks) : num_tasks{num_tasks} {} + +void FAILOVER_SYNC::increment_task() { + std::lock_guard lock(mutex_); + num_tasks++; +} + +void FAILOVER_SYNC::mark_as_complete(bool cancel_other_tasks) { + std::lock_guard lock(mutex_); + if (cancel_other_tasks) { + num_tasks = 0; + } else { + if (num_tasks <= 0) { + throw std::runtime_error("Trying to cancel a failover process that is already done."); + } + num_tasks--; + } + + cv.notify_one(); +} + +void FAILOVER_SYNC::wait_and_complete(int milliseconds) { + std::unique_lock lock(mutex_); + cv.wait_for(lock, std::chrono::milliseconds(milliseconds), [this] { return num_tasks <= 0; }); + num_tasks = 0; +} + +bool FAILOVER_SYNC::is_completed() { + std::unique_lock lock(mutex_); + return num_tasks <= 0; +} + +// ************* FAILOVER *********************************** +// Base class of two writer failover task handlers +FAILOVER::FAILOVER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + unsigned long dbc_id, bool enable_logging) + : connection_handler{connection_handler}, + topology_service{topology_service}, + dbc_id{dbc_id}, + new_connection{nullptr} { + + if (enable_logging) + logger = init_log_file(); +} + +bool FAILOVER::is_writer_connected() { + return new_connection && new_connection->is_connected(); +} + +bool FAILOVER::connect(const std::shared_ptr& host_info) { + new_connection = connection_handler->connect(host_info); + return is_writer_connected(); +} + +void FAILOVER::sleep(int miliseconds) { + std::this_thread::sleep_for(std::chrono::milliseconds(miliseconds)); +} + +// Close new connection if not needed (other task finishes and returns first) +void FAILOVER::release_new_connection() { + if (new_connection) { + new_connection->delete_ds(); + delete new_connection; + new_connection = nullptr; + } +} + +// ************************ RECONNECT_TO_WRITER_HANDLER +// handler reconnecting to a given host, e.g. reconnect to a current writer +RECONNECT_TO_WRITER_HANDLER::RECONNECT_TO_WRITER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + int connection_interval, unsigned long dbc_id, bool enable_logging) + : FAILOVER{connection_handler, topology_service, dbc_id, enable_logging}, + reconnect_interval_ms{connection_interval} {} + +RECONNECT_TO_WRITER_HANDLER::~RECONNECT_TO_WRITER_HANDLER() {} + +void RECONNECT_TO_WRITER_HANDLER::operator()( + std::shared_ptr original_writer, + FAILOVER_SYNC& f_sync, + WRITER_FAILOVER_RESULT& result) { + + if (original_writer) { + MYLOG_TRACE(logger.get(), dbc_id, + "[RECONNECT_TO_WRITER_HANDLER] [TaskA] Attempting to " + "re-connect to the current writer instance: %s", + original_writer->get_host_port_pair().c_str()); + + while (!f_sync.is_completed()) { + if (connect(original_writer)) { + auto latest_topology = + topology_service->get_topology(new_connection, true); + if (latest_topology->total_hosts() > 0 && + is_current_host_writer(original_writer, latest_topology)) { + + topology_service->mark_host_up(original_writer); + if (f_sync.is_completed()) { + break; + } + result = WRITER_FAILOVER_RESULT(true, false, latest_topology, + std::move(new_connection)); + f_sync.mark_as_complete(true); + MYLOG_TRACE(logger.get(), dbc_id, "[RECONNECT_TO_WRITER_HANDLER] [TaskA] Finished"); + return; + } + release_new_connection(); + } + sleep(reconnect_interval_ms); + } + MYLOG_TRACE(logger.get(), dbc_id, "[RECONNECT_TO_WRITER_HANDLER] [TaskA] Cancelled"); + } + // Another thread finishes or both timeout, this thread is canceled + release_new_connection(); + MYLOG_TRACE(logger.get(), dbc_id, "[RECONNECT_TO_WRITER_HANDLER] [TaskA] Finished"); +} + +bool RECONNECT_TO_WRITER_HANDLER::is_current_host_writer( + const std::shared_ptr& original_writer, + const std::shared_ptr& latest_topology) { + auto original_instance = original_writer->instance_name; + if (original_instance.empty()) return false; + auto latest_writer = latest_topology->get_writer(); + auto latest_instance = latest_writer->instance_name; + return latest_instance == original_instance; +} + +// ************************ WAIT_NEW_WRITER_HANDLER +// handler getting the latest cluster topology and connecting to a newly elected +// writer +WAIT_NEW_WRITER_HANDLER::WAIT_NEW_WRITER_HANDLER( + std::shared_ptr connection_handler, + std::shared_ptr topology_service, + std::shared_ptr current_topology, + std::shared_ptr reader_handler, + int connection_interval, unsigned long dbc_id, bool enable_logging) + : FAILOVER{connection_handler, topology_service, dbc_id, enable_logging}, + current_topology{current_topology}, + reader_handler{reader_handler}, + read_topology_interval_ms{connection_interval} {} + +WAIT_NEW_WRITER_HANDLER::~WAIT_NEW_WRITER_HANDLER() {} + +void WAIT_NEW_WRITER_HANDLER::operator()( + std::shared_ptr original_writer, + FAILOVER_SYNC& f_sync, + WRITER_FAILOVER_RESULT& result) { + + MYLOG_TRACE(logger.get(), dbc_id, "[WAIT_NEW_WRITER_HANDLER] [TaskB] Attempting to connect to a new writer instance"); + + while (!f_sync.is_completed()) { + if (!is_writer_connected()) { + connect_to_reader(f_sync); + refresh_topology_and_connect_to_new_writer(original_writer, f_sync); + clean_up_reader_connection(); + } else { + result = WRITER_FAILOVER_RESULT(true, true, current_topology, + std::move(new_connection)); + f_sync.mark_as_complete(true); + MYLOG_TRACE(logger.get(), dbc_id, "[WAIT_NEW_WRITER_HANDLER] [TaskB] Finished"); + return; + } + } + MYLOG_TRACE(logger.get(), dbc_id, "[WAIT_NEW_WRITER_HANDLER] [TaskB] Cancelled"); + + // Another thread finishes or both timeout, this thread is canceled + clean_up_reader_connection(); + release_new_connection(); + MYLOG_TRACE(logger.get(), dbc_id, "[WAIT_NEW_WRITER_HANDLER] [TaskB] Finished"); +} + +// Connect to a reader to later retrieve the latest topology +void WAIT_NEW_WRITER_HANDLER::connect_to_reader(FAILOVER_SYNC& f_sync) { + while (!f_sync.is_completed()) { + auto connection_result = reader_handler->get_reader_connection(current_topology, f_sync); + if (connection_result.connected && connection_result.new_connection->is_connected()) { + reader_connection = connection_result.new_connection; + current_reader_host = connection_result.new_host; + MYLOG_TRACE( + logger.get(), dbc_id, + "[WAIT_NEW_WRITER_HANDLER] [TaskB] Connected to reader: %s", + connection_result.new_host->get_host_port_pair().c_str()); + break; + } + MYLOG_TRACE(logger.get(), dbc_id, "[WAIT_NEW_WRITER_HANDLER] [TaskB] Failed to connect to any reader."); + } +} + +// Use just connected reader to refresh topology and try to connect to a new writer +void WAIT_NEW_WRITER_HANDLER::refresh_topology_and_connect_to_new_writer( + std::shared_ptr original_writer, FAILOVER_SYNC& f_sync) { + while (!f_sync.is_completed()) { + auto latest_topology = topology_service->get_topology(reader_connection, true); + if (latest_topology->total_hosts() > 0) { + current_topology = latest_topology; + auto writer_candidate = current_topology->get_writer(); + // Same case is handled by the reconnect handler + if (!HOST_INFO::is_host_same(writer_candidate, original_writer)) { + if (connect_to_writer(writer_candidate)) return; + } + } + sleep(read_topology_interval_ms); + } +} + +// Try to connect to the writer candidate +bool WAIT_NEW_WRITER_HANDLER::connect_to_writer( + std::shared_ptr writer_candidate) { + + MYLOG_TRACE(logger.get(), dbc_id, + "[WAIT_NEW_WRITER_HANDLER] [TaskB] Trying to connect to a new writer: %s", + writer_candidate->get_host_port_pair().c_str()); + + if (HOST_INFO::is_host_same(writer_candidate, current_reader_host)) { + new_connection = reader_connection; + } else if (!connect(writer_candidate)) { + topology_service->mark_host_down(writer_candidate); + return false; + } + topology_service->mark_host_up(writer_candidate); + return true; +} + +// Close reader connection if not needed (open and not the same as current connection) +void WAIT_NEW_WRITER_HANDLER::clean_up_reader_connection() { + if (reader_connection && new_connection != reader_connection) { + reader_connection->delete_ds(); + delete reader_connection; + reader_connection = nullptr; + } +} + +// ************************** FAILOVER_WRITER_HANDLER ************************** + +FAILOVER_WRITER_HANDLER::FAILOVER_WRITER_HANDLER( + std::shared_ptr topology_service, + std::shared_ptr reader_handler, + std::shared_ptr connection_handler, + int writer_failover_timeout_ms, int read_topology_interval_ms, + int reconnect_writer_interval_ms, unsigned long dbc_id, bool enable_logging) + : connection_handler{connection_handler}, + topology_service{topology_service}, + reader_handler{reader_handler}, + writer_failover_timeout_ms{writer_failover_timeout_ms}, + read_topology_interval_ms{read_topology_interval_ms}, + reconnect_writer_interval_ms{reconnect_writer_interval_ms}, + dbc_id{dbc_id} { + + if (enable_logging) + logger = init_log_file(); +} + +FAILOVER_WRITER_HANDLER::~FAILOVER_WRITER_HANDLER() {} + +WRITER_FAILOVER_RESULT FAILOVER_WRITER_HANDLER::failover( + std::shared_ptr current_topology) { + + if (!current_topology || current_topology->total_hosts() == 0) { + MYLOG_TRACE(logger.get(), dbc_id, + "[FAILOVER_WRITER_HANDLER] Failover was called with " + "an invalid (null or empty) topology"); + return WRITER_FAILOVER_RESULT(false, false, nullptr, nullptr); + } + + FAILOVER_SYNC failover_sync(2); + // Constructing the function objects + RECONNECT_TO_WRITER_HANDLER reconnect_handler( + connection_handler, topology_service, reconnect_writer_interval_ms, dbc_id, logger != nullptr); + WAIT_NEW_WRITER_HANDLER new_writer_handler( + connection_handler, topology_service, current_topology, reader_handler, + read_topology_interval_ms, dbc_id, logger != nullptr); + + auto original_writer = current_topology->get_writer(); + topology_service->mark_host_down(original_writer); + + auto reconnect_result = WRITER_FAILOVER_RESULT(false, false, nullptr, nullptr); + auto new_writer_result = WRITER_FAILOVER_RESULT(false, false, nullptr, nullptr); + + // Try reconnecting to the original writer host + auto reconnect_future = std::async(std::launch::async, std::ref(reconnect_handler), + original_writer, std::ref(failover_sync), + std::ref(reconnect_result)); + + // Concurrently see if topology has changed and try connecting to a new writer + auto new_writer_future = std::async(std::launch::async, std::ref(new_writer_handler), + std::cref(original_writer), std::ref(failover_sync), + std::ref(new_writer_result)); + + failover_sync.wait_and_complete(writer_failover_timeout_ms); + + if (reconnect_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + reconnect_future.get(); + } + + if (new_writer_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + new_writer_future.get(); + } + + if (reconnect_result.connected) { + MYLOG_TRACE(logger.get(), dbc_id, + "[FAILOVER_WRITER_HANDLER] Successfully re-connected to the current writer instance: %s", + reconnect_result.new_topology->get_writer()->get_host_port_pair().c_str()); + return reconnect_result; + } else if (new_writer_result.connected) { + MYLOG_TRACE(logger.get(), dbc_id, + "[FAILOVER_WRITER_HANDLER] Successfully connected to the new writer instance: %s", + new_writer_result.new_topology->get_writer()->get_host_port_pair().c_str()); + return new_writer_result; + } + + // timeout + MYLOG_TRACE(logger.get(), dbc_id, "[FAILOVER_WRITER_HANDLER] Failed to connect to the writer instance."); + return WRITER_FAILOVER_RESULT(false, false, nullptr, nullptr); +} diff --git a/driver/handle.cc b/driver/handle.cc index f42e777d..3a669d58 100644 --- a/driver/handle.cc +++ b/driver/handle.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -69,9 +71,12 @@ bool ENV::has_connections() return conn_list.size() > 0; } -DBC::DBC(ENV *p_env) : env(p_env), mysql(nullptr), - txn_isolation(DEFAULT_TXN_ISOLATION), - last_query_time((time_t) time((time_t*) 0)) +DBC::DBC(ENV *p_env) + : id{last_dbc_id++}, + env(p_env), + mysql_proxy(nullptr), + txn_isolation(DEFAULT_TXN_ISOLATION), + last_query_time((time_t)time((time_t *)0)) { //mysql->net.vio = nullptr; myodbc_ov_init(env->odbc_ver); @@ -103,9 +108,7 @@ void DBC::free_explicit_descriptors() void DBC::close() { - if (mysql) - mysql_close(mysql); - mysql = nullptr; + mysql_proxy->close(); } DBC::~DBC() @@ -116,6 +119,12 @@ DBC::~DBC() if (ds) ds_delete(ds); + if (mysql_proxy) + delete mysql_proxy; + + if (fh) + delete fh; + free_explicit_descriptors(); } @@ -131,7 +140,7 @@ SQLRETURN DBC::set_error(char * state, const char * message, uint errcode) SQLRETURN DBC::set_error(char * state) { - return set_error(state, mysql_error(mysql), mysql_errno(mysql)); + return set_error(state, mysql_proxy->error(), mysql_proxy->error_code()); } @@ -146,7 +155,10 @@ SQLRETURN SQL_API my_SQLAllocEnv(SQLHENV *phenv) ENV *env; std::lock_guard env_guard(g_lock); - myodbc_init(); // This will call mysql_library_init() +#ifndef _UNIX_ +#else + myodbc_init(); // This will call mysql_library_init() +#endif /* _UNIX_ */ #ifndef USE_IODBC env = new ENV(SQL_OV_ODBC3_80); @@ -180,10 +192,10 @@ SQLRETURN SQL_API my_SQLFreeEnv(SQLHENV henv) { ENV *env= (ENV *) henv; delete env; -#ifndef _UNIX_ -#else +#ifdef _UNIX_ myodbc_end(); #endif /* _UNIX_ */ + MONITOR_THREAD_CONTAINER::release_instance(); return(SQL_SUCCESS); } @@ -236,10 +248,12 @@ SQLRETURN SQL_API my_SQLAllocConnect(SQLHENV henv, SQLHDBC *phdbc) ++thread_count; - if (mysql_get_client_version() < MIN_MYSQL_VERSION) + if (MYSQL_PROXY::get_client_version() < MIN_MYSQL_VERSION) { char buff[255]; - sprintf(buff, "Wrong libmysqlclient library version: %ld. MyODBC needs at least version: %ld", mysql_get_client_version(), MIN_MYSQL_VERSION); + snprintf(buff, sizeof(buff), + "Wrong libmysqlclient library version: %ld. MyODBC needs at least version: %ld", + MYSQL_PROXY::get_client_version(), MIN_MYSQL_VERSION); return(set_env_error((ENV*)henv, MYERR_S1000, buff, 0)); } @@ -302,33 +316,33 @@ int wakeup_connection(DBC *dbc) { ds_get_utf8attr(ds->pwd1, &ds->pwd18); int fator = 2; - mysql_options4(dbc->mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - ds->pwd18); + dbc->mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + ds->pwd18); } if(ds->pwd2 && ds->pwd2[0]) { ds_get_utf8attr(ds->pwd2, &ds->pwd28); int fator = 2; - mysql_options4(dbc->mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - ds->pwd28); + dbc->mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + ds->pwd28); } if(ds->pwd3 && ds->pwd3[0]) { ds_get_utf8attr(ds->pwd3, &ds->pwd38); int fator = 3; - mysql_options4(dbc->mysql, MYSQL_OPT_USER_PASSWORD, - &fator, - ds->pwd38); + dbc->mysql_proxy->options4(MYSQL_OPT_USER_PASSWORD, + &fator, + ds->pwd38); } #endif - if (mysql_change_user(dbc->mysql, ds_get_utf8attr(ds->uid, &ds->uid8), - ds_get_utf8attr(ds->pwd, &ds->pwd8), - ds_get_utf8attr(ds->database, &ds->database8))) + if (dbc->mysql_proxy->change_user(ds_get_utf8attr(ds->uid, &ds->uid8), + ds_get_utf8attr(ds->pwd, &ds->pwd8), + ds_get_utf8attr(ds->database, &ds->database8))) { return 1; } @@ -406,9 +420,6 @@ int adjust_param_bind_array(STMT *stmt) */ SQLRETURN SQL_API my_SQLAllocStmt(SQLHDBC hdbc,SQLHSTMT *phstmt) { -#ifndef _UNIX_ - HGLOBAL hstmt; -#endif std::unique_ptr stmt; DBC *dbc= (DBC*) hdbc; diff --git a/driver/host_info.cc b/driver/host_info.cc new file mode 100644 index 00000000..05ef6348 --- /dev/null +++ b/driver/host_info.cc @@ -0,0 +1,118 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "host_info.h" + +// TODO +// the entire HOST_INFO needs to be reviewed based on needed interfaces and other objects like CLUSTER_TOPOLOGY_INFO +// most/all of the HOST_INFO potentially could be internal to CLUSTER_TOPOLOGY_INFO and specfic information may be accessed +// through CLUSTER_TOPOLOGY_INFO +// Move the implementation to it's own file + +HOST_INFO::HOST_INFO() : HOST_INFO("", NO_PORT) {} + +HOST_INFO::HOST_INFO(std::string host, int port) + : HOST_INFO(host, port, UP, false) {} + +// would need some checks for nulls +HOST_INFO::HOST_INFO(const char* host, int port) + : HOST_INFO(host, port, UP, false) {} + +HOST_INFO::HOST_INFO(std::string host, int port, HOST_STATE state, bool is_writer) + : host{ host }, port{ port }, host_state{ state }, is_writer{ is_writer } +{ +} + +// would need some checks for nulls +HOST_INFO::HOST_INFO(const char* host, int port, HOST_STATE state, bool is_writer) + : host{ host }, port{ port }, host_state{ state }, is_writer{ is_writer } +{ +} + +HOST_INFO::~HOST_INFO() {} + +/** + * Returns the host. + * + * @return the host + */ +std::string HOST_INFO::get_host() { + return host; +} + +/** + * Returns the port. + * + * @return the port + */ +int HOST_INFO::get_port() { + return port; +} + +/** + * Returns a host:port representation of this host. + * + * @return the host:port representation of this host + */ +std::string HOST_INFO::get_host_port_pair() { + return get_host() + HOST_PORT_SEPARATOR + std::to_string(get_port()); +} + + +bool HOST_INFO::equal_host_port_pair(HOST_INFO& hi) { + return get_host_port_pair() == hi.get_host_port_pair(); +} + +HOST_STATE HOST_INFO::get_host_state() { + return host_state; +} + +void HOST_INFO::set_host_state(HOST_STATE state) { + host_state = state; +} + +bool HOST_INFO::is_host_up() { + return host_state == UP; +} + +bool HOST_INFO::is_host_down() { + return host_state == DOWN; +} + +bool HOST_INFO::is_host_writer() { + return is_writer; +} +void HOST_INFO::mark_as_writer(bool writer) { + is_writer = writer; +} + +// Check if two host info have same instance name +bool HOST_INFO::is_host_same(const std::shared_ptr& h1, const std::shared_ptr& h2) { + return h1->instance_name == h2->instance_name; +} diff --git a/driver/host_info.h b/driver/host_info.h new file mode 100644 index 00000000..d9d442ed --- /dev/null +++ b/driver/host_info.h @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __HOSTINFO_H__ +#define __HOSTINFO_H__ + +#include +#include + +enum HOST_STATE { UP, DOWN }; + +// TODO Think about char types. Using strings for now, but should SQLCHAR *, or CHAR * be employed? +// Most of the strings are for internal failover things +class HOST_INFO { +public: + HOST_INFO(); + //TODO - probably choose one of the following constructors, or more precisely choose which data type they should take + HOST_INFO(std::string host, int port); + HOST_INFO(const char* host, int port); + HOST_INFO(std::string host, int port, HOST_STATE state, bool is_writer); + HOST_INFO(const char* host, int port, HOST_STATE state, bool is_writer); + ~HOST_INFO(); + + int get_port(); + std::string get_host(); + std::string get_host_port_pair(); + bool equal_host_port_pair(HOST_INFO& hi); + HOST_STATE get_host_state(); + void set_host_state(HOST_STATE state); + bool is_host_up(); + bool is_host_down(); + bool is_host_writer(); + void mark_as_writer(bool writer); + static bool is_host_same(const std::shared_ptr& h1, const std::shared_ptr& h2); + static constexpr int NO_PORT = -1; + + // used to be properties - TODO - remove the ones that are not necessary + std::string session_id; + std::string last_updated; + std::string replica_lag; + std::string instance_name; + +private: + const std::string HOST_PORT_SEPARATOR = ":"; + const std::string host; + const int port = NO_PORT; + + HOST_STATE host_state; + bool is_writer; +}; + +#endif /* __HOSTINFO_H__ */ diff --git a/driver/info.cc b/driver/info.cc index 17c7b850..d11d6e7c 100644 --- a/driver/info.cc +++ b/driver/info.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -133,7 +135,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, 0); case SQL_COLLATION_SEQ: - MYINFO_SET_STR(dbc->mysql->charset->name); + MYINFO_SET_STR(dbc->mysql_proxy->get_character_set()->name); case SQL_COLUMN_ALIAS: MYINFO_SET_STR("Y"); @@ -199,7 +201,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, case SQL_CREATE_VIEW: /** @todo SQL_CV_LOCAL ? */ - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_ULONG(SQL_CV_CREATE_VIEW | SQL_CV_CHECK_OPTION | SQL_CV_CASCADED); else @@ -226,7 +228,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_STR("N"); case SQL_DATABASE_NAME: - if (is_connected(dbc) && reget_current_catalog(dbc)) + if (dbc->mysql_proxy->is_connected() && reget_current_catalog(dbc)) return dbc->set_error("HY000", "SQLGetInfo() failed to return current catalog.", 0); @@ -241,7 +243,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, case SQL_DBMS_VER: /** @todo technically this is not right: should be ##.##.#### */ - MYINFO_SET_STR(dbc->mysql->server_version); + MYINFO_SET_STR(dbc->mysql_proxy->get_server_version()); case SQL_DDL_INDEX: MYINFO_SET_ULONG(SQL_DI_CREATE_INDEX | SQL_DI_DROP_INDEX); @@ -284,7 +286,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_ULONG(SQL_DT_DROP_TABLE | SQL_DT_CASCADE | SQL_DT_RESTRICT); case SQL_DROP_VIEW: - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_ULONG(SQL_DV_DROP_VIEW | SQL_DV_CASCADE | SQL_DV_RESTRICT); else MYINFO_SET_ULONG(0); @@ -359,7 +361,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, We have INFORMATION_SCHEMA.SCHEMATA, but we don't report it because the driver exposes databases (schema) as catalogs. */ - if (is_minimum_version(dbc->mysql->server_version, "5.1")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.1")) MYINFO_SET_ULONG(SQL_ISV_CHARACTER_SETS | SQL_ISV_COLLATIONS | SQL_ISV_COLUMN_PRIVILEGES | SQL_ISV_COLUMNS | SQL_ISV_KEY_COLUMN_USAGE | @@ -367,7 +369,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, /* SQL_ISV_SCHEMATA | */ SQL_ISV_TABLE_CONSTRAINTS | SQL_ISV_TABLE_PRIVILEGES | SQL_ISV_TABLES | SQL_ISV_VIEWS); - else if (is_minimum_version(dbc->mysql->server_version, "5.0")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_ULONG(SQL_ISV_CHARACTER_SETS | SQL_ISV_COLLATIONS | SQL_ISV_COLUMN_PRIVILEGES | SQL_ISV_COLUMNS | SQL_ISV_KEY_COLUMN_USAGE | /* SQL_ISV_SCHEMATA | */ @@ -393,7 +395,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, the MySQL Reference Manual (which is, in turn, generated from the source) with the pre-reserved ODBC keywords removed. */ - if (is_minimum_version(dbc->mysql->server_version, "8.0.22")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "8.0.22")) MYINFO_SET_STR("ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB," "CALL,CHANGE,CONDITION,DATABASE,DATABASES,DAY_HOUR," "DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED," @@ -418,7 +420,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, "TINYBLOB,TINYINT,TINYTEXT,TRIGGER,UNDO,UNLOCK,UNSIGNED," "USE,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VARBINARY," "VARCHARACTER,WHILE,X509,XOR,YEAR_MONTH,ZEROFILL"); - else if (is_minimum_version(dbc->mysql->server_version, "5.7")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.7")) MYINFO_SET_STR("ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB," "CALL,CHANGE,CONDITION,DATABASE,DATABASES,DAY_HOUR," "DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED," @@ -443,7 +445,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, "TINYBLOB,TINYINT,TINYTEXT,TRIGGER,UNDO,UNLOCK,UNSIGNED," "USE,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VARBINARY," "VARCHARACTER,WHILE,X509,XOR,YEAR_MONTH,ZEROFILL"); - else if (is_minimum_version(dbc->mysql->server_version, "5.6")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.6")) MYINFO_SET_STR("ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB," "CALL,CHANGE,CONDITION,DATABASE,DATABASES,DAY_HOUR," "DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED," @@ -468,7 +470,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, "TINYBLOB,TINYINT,TINYTEXT,TRIGGER,UNDO,UNLOCK,UNSIGNED," "USE,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VARBINARY," "VARCHARACTER,WHILE,X509,XOR,YEAR_MONTH,ZEROFILL"); - else if (is_minimum_version(dbc->mysql->server_version, "5.5")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.5")) MYINFO_SET_STR("ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB," "CALL,CHANGE,CONDITION,DATABASE,DATABASES,DAY_HOUR," "DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED," @@ -491,7 +493,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, "TINYBLOB,TINYINT,TINYTEXT,TRIGGER,UNDO,UNLOCK,UNSIGNED," "USE,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VARBINARY," "VARCHARACTER,WHILE,X509,XOR,YEAR_MONTH,ZEROFILL"); - else if (is_minimum_version(dbc->mysql->server_version, "5.1")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.1")) MYINFO_SET_STR("ACCESSIBLE,ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB," "CALL,CHANGE,CONDITION,DATABASE,DATABASES,DAY_HOUR," "DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DELAYED," @@ -513,7 +515,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, "TINYTEXT,TRIGGER,UNDO,UNLOCK,UNSIGNED,USE,UTC_DATE," "UTC_TIME,UTC_TIMESTAMP,VARBINARY,VARCHARACTER,WHILE,X509," "XOR,YEAR_MONTH,ZEROFILL"); - else if (is_minimum_version(dbc->mysql->server_version, "5.0")) + else if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_STR("ANALYZE,ASENSITIVE,BEFORE,BIGINT,BINARY,BLOB,CALL,CHANGE," "CONDITION,DATABASE,DATABASES,DAY_HOUR,DAY_MICROSECOND," "DAY_MINUTE,DAY_SECOND,DELAYED,DETERMINISTIC,DISTINCTROW," @@ -602,7 +604,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_USHORT(NAME_LEN); case SQL_MAX_INDEX_SIZE: - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_USHORT(3072); else MYINFO_SET_USHORT(1024); @@ -630,7 +632,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_USHORT(NAME_LEN); case SQL_MAX_TABLES_IN_SELECT: - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_USHORT(63); else MYINFO_SET_USHORT(31); @@ -688,13 +690,13 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_ULONG(SQL_PAS_NO_BATCH); case SQL_PROCEDURE_TERM: - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_STR("stored procedure"); else MYINFO_SET_STR(""); case SQL_PROCEDURES: - if (is_minimum_version(dbc->mysql->server_version, "5.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "5.0")) MYINFO_SET_STR("Y"); else MYINFO_SET_STR("N"); @@ -733,7 +735,7 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, MYINFO_SET_STR("\\"); case SQL_SERVER_NAME: - MYINFO_SET_STR(dbc->mysql->host_info); + MYINFO_SET_STR(dbc->mysql_proxy->get_host_info()); case SQL_SPECIAL_CHARACTERS: /* We can handle anything but / and \xff. */ @@ -910,12 +912,10 @@ MySQLGetInfo(SQLHDBC hdbc, SQLUSMALLINT fInfoType, default: { char buff[80]; - sprintf(buff, "Unsupported option: %d to SQLGetInfo", fInfoType); + snprintf(buff, sizeof(buff), "Unsupported option: %d to SQLGetInfo", fInfoType); return set_conn_error((DBC*)hdbc, MYERR_S1C00, buff, 4000); } } - - return SQL_SUCCESS; } diff --git a/driver/monitor.cc b/driver/monitor.cc new file mode 100644 index 00000000..a6323272 --- /dev/null +++ b/driver/monitor.cc @@ -0,0 +1,237 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "monitor.h" +#include "monitor_service.h" +#include "mylog.h" +#include "mysql_proxy.h" + +MONITOR::MONITOR( + std::shared_ptr host_info, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds monitor_disposal_time, + DataSource* ds, + bool enable_logging) + : MONITOR( + std::move(host_info), + failure_detection_timeout, + monitor_disposal_time, + new MYSQL_MONITOR_PROXY(ds), + enable_logging) {}; + +MONITOR::MONITOR( + std::shared_ptr host_info, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds monitor_disposal_time, + MYSQL_MONITOR_PROXY* proxy, + bool enable_logging) { + + this->host = std::move(host_info); + this->failure_detection_timeout = failure_detection_timeout; + this->disposal_time = monitor_disposal_time; + this->mysql_proxy = proxy; + this->connection_check_interval = (std::chrono::milliseconds::max)(); + if (enable_logging) + this->logger = init_log_file(); +} + +MONITOR::~MONITOR() { + if (this->mysql_proxy) { + delete this->mysql_proxy; + this->mysql_proxy = nullptr; + } +} + +void MONITOR::start_monitoring(std::shared_ptr context) { + std::chrono::milliseconds detection_interval = context->get_failure_detection_interval(); + if (detection_interval < this->connection_check_interval) { + this->connection_check_interval = detection_interval; + } + + auto current_time = get_current_time(); + context->set_start_monitor_time(current_time); + this->last_context_timestamp = current_time; + + { + std::unique_lock lock(mutex_); + this->contexts.push_back(context); + } +} + +void MONITOR::stop_monitoring(std::shared_ptr context) { + if (context == nullptr) { + MYLOG_TRACE( + this->logger.get(), 0, + "[MONITOR] Invalid context passed into stop_monitoring()"); + return; + } + + context->invalidate(); + + { + std::unique_lock lock(mutex_); + this->contexts.remove(context); + } + + this->connection_check_interval = this->find_shortest_interval(); +} + +bool MONITOR::is_stopped() { + return this->stopped.load(); +} + +void MONITOR::stop() { + this->stopped.store(true); +} + +void MONITOR::clear_contexts() { + { + std::unique_lock lock(mutex_); + this->contexts.clear(); + } + + this->connection_check_interval = (std::chrono::milliseconds::max)(); +} + +// Periodically ping the server and update the contexts' connection status. +void MONITOR::run(std::shared_ptr service) { + this->stopped = false; + while (!this->stopped) { + bool have_contexts; + { + std::unique_lock lock(mutex_); + have_contexts = !this->contexts.empty(); + } + if (have_contexts) { + auto status_check_start_time = this->get_current_time(); + this->last_context_timestamp = status_check_start_time; + + CONNECTION_STATUS status = this->check_connection_status(); + + { + std::unique_lock lock(mutex_); + for (auto it = this->contexts.begin(); it != this->contexts.end(); ++it) { + std::shared_ptr context = *it; + context->update_connection_status( + status_check_start_time, + status_check_start_time + status.elapsed_time, + status.is_valid); + } + } + + std::chrono::milliseconds check_interval = this->get_connection_check_interval(); + auto sleep_time = check_interval - status.elapsed_time; + if (sleep_time > std::chrono::milliseconds(0)) { + std::this_thread::sleep_for(sleep_time); + } + } + else { + auto time_inactive = std::chrono::duration_cast(this->get_current_time() - this->last_context_timestamp); + if (time_inactive >= this->disposal_time) { + break; + } + std::this_thread::sleep_for(thread_sleep_when_inactive); + } + } + + service->notify_unused(shared_from_this()); + + this->stopped = true; +} + +std::chrono::milliseconds MONITOR::get_connection_check_interval() { + std::unique_lock lock(mutex_); + if (this->contexts.empty()) { + return std::chrono::milliseconds(0); + } + + return this->connection_check_interval; +} + +CONNECTION_STATUS MONITOR::check_connection_status() { + if (this->mysql_proxy == nullptr || !this->mysql_proxy->is_connected()) { + const auto start = this->get_current_time(); + bool connected = this->connect(); + return CONNECTION_STATUS{ + connected, + std::chrono::duration_cast(this->get_current_time() - start) + }; + } + + auto start = this->get_current_time(); + bool is_connection_active = this->mysql_proxy->ping() == 0; + auto duration = this->get_current_time() - start; + + return CONNECTION_STATUS{ + is_connection_active, + std::chrono::duration_cast(duration) + }; +} + +bool MONITOR::connect() { + this->mysql_proxy->close(); + this->mysql_proxy->init(); + + // Timeout shouldn't be 0 by now, but double check just in case + unsigned int timeout_sec = this->failure_detection_timeout.count() == 0 ? failure_detection_timeout_default : this->failure_detection_timeout.count(); + this->mysql_proxy->options(MYSQL_OPT_CONNECT_TIMEOUT, &timeout_sec); + this->mysql_proxy->options(MYSQL_OPT_READ_TIMEOUT, &timeout_sec); + + if (!this->mysql_proxy->connect()) { + MYLOG_TRACE(this->logger.get(), 0, this->mysql_proxy->error()); + this->mysql_proxy->close(); + return false; + } + + return true; +} + +std::chrono::milliseconds MONITOR::find_shortest_interval() { + auto min = (std::chrono::milliseconds::max)(); + + { + std::unique_lock lock(mutex_); + if (this->contexts.empty()) { + return min; + } + + for (auto it = this->contexts.begin(); it != this->contexts.end(); ++it) { + auto failure_detection_interval = (*it)->get_failure_detection_interval(); + if (failure_detection_interval < min) { + min = failure_detection_interval; + } + } + } + + return min; +} + +std::chrono::steady_clock::time_point MONITOR::get_current_time() { + return std::chrono::steady_clock::now(); +} diff --git a/driver/monitor.h b/driver/monitor.h new file mode 100644 index 00000000..761db287 --- /dev/null +++ b/driver/monitor.h @@ -0,0 +1,101 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MONITOR_H__ +#define __MONITOR_H__ + +#include "host_info.h" +#include "monitor_connection_context.h" + +#include +#include + +struct CONNECTION_STATUS { + bool is_valid; + std::chrono::milliseconds elapsed_time; +}; + +struct DataSource; +class MONITOR_SERVICE; +class MYSQL_MONITOR_PROXY; + + +namespace { + const std::chrono::milliseconds thread_sleep_when_inactive = std::chrono::milliseconds(100); + const unsigned int failure_detection_timeout_default = 5; +} + +class MONITOR : public std::enable_shared_from_this { +public: + MONITOR( + std::shared_ptr host_info, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds monitor_disposal_time, + DataSource* ds, + bool enable_logging = false); + MONITOR( + std::shared_ptr host_info, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds monitor_disposal_time, + MYSQL_MONITOR_PROXY* proxy, + bool enable_logging = false); + virtual ~MONITOR(); + + virtual void start_monitoring(std::shared_ptr context); + virtual void stop_monitoring(std::shared_ptr context); + virtual bool is_stopped(); + virtual void clear_contexts(); + virtual void run(std::shared_ptr service); + void stop(); + +private: + std::atomic_bool stopped{true}; + std::shared_ptr host; + std::chrono::milliseconds connection_check_interval; + std::chrono::seconds failure_detection_timeout; + std::chrono::milliseconds disposal_time; + std::list> contexts; + std::chrono::steady_clock::time_point last_context_timestamp; + MYSQL_MONITOR_PROXY* mysql_proxy = nullptr; + std::shared_ptr logger; + std::mutex mutex_; + + std::chrono::milliseconds get_connection_check_interval(); + CONNECTION_STATUS check_connection_status(); + bool connect(); + std::chrono::milliseconds find_shortest_interval(); + virtual std::chrono::steady_clock::time_point get_current_time(); + +#ifdef UNIT_TEST_BUILD + // Allows for testing private methods + friend class TEST_UTILS; +#endif +}; + +#endif /* __MONITOR_H__ */ diff --git a/driver/monitor_connection_context.cc b/driver/monitor_connection_context.cc new file mode 100644 index 00000000..dc458ab9 --- /dev/null +++ b/driver/monitor_connection_context.cc @@ -0,0 +1,209 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "monitor_connection_context.h" +#include "driver.h" + +#include +#include + +MONITOR_CONNECTION_CONTEXT::MONITOR_CONNECTION_CONTEXT(DBC* connection_to_abort, + std::set node_keys, + std::chrono::milliseconds failure_detection_time, + std::chrono::milliseconds failure_detection_interval, + int failure_detection_count, + bool enable_logging) : connection_to_abort{connection_to_abort}, + node_keys{node_keys}, + failure_detection_time{failure_detection_time}, + failure_detection_interval{failure_detection_interval}, + failure_detection_count{failure_detection_count}, + failure_count{0}, + node_unhealthy{false} { + if (enable_logging) + this->logger = init_log_file(); +} + +MONITOR_CONNECTION_CONTEXT::~MONITOR_CONNECTION_CONTEXT() {} + +std::chrono::steady_clock::time_point MONITOR_CONNECTION_CONTEXT::get_start_monitor_time() { + return start_monitor_time; +} + +void MONITOR_CONNECTION_CONTEXT::set_start_monitor_time(std::chrono::steady_clock::time_point time) { + start_monitor_time = time; +} + +std::set MONITOR_CONNECTION_CONTEXT::get_node_keys() { + return node_keys; +} + +std::chrono::milliseconds MONITOR_CONNECTION_CONTEXT::get_failure_detection_time() { + return failure_detection_time; +} + +std::chrono::milliseconds MONITOR_CONNECTION_CONTEXT::get_failure_detection_interval() { + return failure_detection_interval; +} + +int MONITOR_CONNECTION_CONTEXT::get_failure_detection_count() { + return failure_detection_count; +} + +int MONITOR_CONNECTION_CONTEXT::get_failure_count() { + return failure_count; +} + +void MONITOR_CONNECTION_CONTEXT::set_failure_count(int count) { + failure_count = count; +} + +void MONITOR_CONNECTION_CONTEXT::increment_failure_count() { + failure_count++; +} + +void MONITOR_CONNECTION_CONTEXT::set_invalid_node_start_time(std::chrono::steady_clock::time_point time) { + invalid_node_start_time = time; +} + +void MONITOR_CONNECTION_CONTEXT::reset_invalid_node_start_time() { + std::chrono::steady_clock::time_point timestamp_zero{}; + invalid_node_start_time = timestamp_zero; +} + +bool MONITOR_CONNECTION_CONTEXT::is_invalid_node_start_time_defined() { + std::chrono::steady_clock::time_point timestamp_zero{}; + return invalid_node_start_time > timestamp_zero; +} + +std::chrono::steady_clock::time_point MONITOR_CONNECTION_CONTEXT::get_invalid_node_start_time() { + return invalid_node_start_time; +} + +bool MONITOR_CONNECTION_CONTEXT::is_node_unhealthy() { + return node_unhealthy; +} + +void MONITOR_CONNECTION_CONTEXT::set_node_unhealthy(bool node) { + node_unhealthy = node; +} + +bool MONITOR_CONNECTION_CONTEXT::is_active_context() { + return active_context.load(); +} + +void MONITOR_CONNECTION_CONTEXT::invalidate() { + active_context.store(false); +} + +DBC* MONITOR_CONNECTION_CONTEXT::get_connection_to_abort() { + return connection_to_abort; +} + +unsigned long MONITOR_CONNECTION_CONTEXT::get_dbc_id() { + return connection_to_abort ? connection_to_abort->id : 0; +} + +// Update whether the connection is still valid if the total elapsed time has passed the grace period. +void MONITOR_CONNECTION_CONTEXT::update_connection_status( + std::chrono::steady_clock::time_point status_check_start_time, + std::chrono::steady_clock::time_point current_time, + bool is_valid) { + + if (!is_active_context()) { + return; + } + + auto total_elapsed_time = current_time - get_start_monitor_time(); + + if (total_elapsed_time > get_failure_detection_time()) { + set_connection_valid(is_valid, status_check_start_time, current_time); + } +} + +// Set whether the connection to the server is still valid based on the monitoring settings. +void MONITOR_CONNECTION_CONTEXT::set_connection_valid( + bool connection_valid, + std::chrono::steady_clock::time_point status_check_start_time, + std::chrono::steady_clock::time_point current_time) { + + const auto node_keys_str = build_node_keys_str(); + if (!connection_valid) { + increment_failure_count(); + + if (!is_invalid_node_start_time_defined()) { + set_invalid_node_start_time(status_check_start_time); + } + + const auto invalid_node_duration_ms = std::chrono::duration_cast(current_time - get_invalid_node_start_time()); + + const auto max_invalid_node_duration = get_failure_detection_interval() * (std::max)(0, get_failure_detection_count()); + + if (invalid_node_duration_ms >= max_invalid_node_duration) { + MYLOG_TRACE(logger.get(), get_dbc_id(), "[MONITOR_CONNECTION_CONTEXT] Node '%s' is *dead*.", node_keys_str.c_str()); + set_node_unhealthy(true); + abort_connection(); + return; + } + + MYLOG_TRACE( + logger.get(), get_dbc_id(), + "[MONITOR_CONNECTION_CONTEXT] Node '%s' is *not responding* (%d).", node_keys_str.c_str(), get_failure_count()); + return; + } + + set_failure_count(0); + reset_invalid_node_start_time(); + set_node_unhealthy(false); + MYLOG_TRACE(logger.get(), get_dbc_id(), "[MONITOR_CONNECTION_CONTEXT] Node '%s' is *alive*.", node_keys_str.c_str()); +} + +void MONITOR_CONNECTION_CONTEXT::abort_connection() { + std::lock_guard lock(mutex_); + if ((!get_connection_to_abort()) || (!is_active_context())) { + return; + } + connection_to_abort->mysql_proxy->close_socket(); +} + +std::string MONITOR_CONNECTION_CONTEXT::build_node_keys_str() { + if (node_keys.empty()) { + return "[]"; + } + + auto it = node_keys.begin(); + std::string node_keys_str = '[' +(*it); + ++it; + while (it != node_keys.end()) { + node_keys_str += ", " + *it; + ++it; + } + node_keys_str += ']'; + + return node_keys_str; +} diff --git a/driver/monitor_connection_context.h b/driver/monitor_connection_context.h new file mode 100644 index 00000000..40f29b5e --- /dev/null +++ b/driver/monitor_connection_context.h @@ -0,0 +1,104 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MONITORCONNECTIONCONTEXT_H__ +#define __MONITORCONNECTIONCONTEXT_H__ + +#include +#include +#include +#include +#include +#include + +struct DBC; + +// Monitoring context for each connection. This contains each connection's criteria for +// whether a server should be considered unhealthy. +class MONITOR_CONNECTION_CONTEXT { +public: + MONITOR_CONNECTION_CONTEXT(DBC* connection_to_abort, + std::set node_keys, + std::chrono::milliseconds failure_detection_time, + std::chrono::milliseconds failure_detection_interval, + int failure_detection_count, + bool enable_logging = false); + virtual ~MONITOR_CONNECTION_CONTEXT(); + + std::chrono::steady_clock::time_point get_start_monitor_time(); + virtual void set_start_monitor_time(std::chrono::steady_clock::time_point time); + std::set get_node_keys(); + std::chrono::milliseconds get_failure_detection_time(); + std::chrono::milliseconds get_failure_detection_interval(); + int get_failure_detection_count(); + int get_failure_count(); + void set_failure_count(int count); + void increment_failure_count(); + void set_invalid_node_start_time(std::chrono::steady_clock::time_point time); + void reset_invalid_node_start_time(); + bool is_invalid_node_start_time_defined(); + std::chrono::steady_clock::time_point get_invalid_node_start_time(); + bool is_node_unhealthy(); + void set_node_unhealthy(bool node); + bool is_active_context(); + void invalidate(); + DBC* get_connection_to_abort(); + unsigned long get_dbc_id(); + + void update_connection_status( + std::chrono::steady_clock::time_point status_check_start_time, + std::chrono::steady_clock::time_point current_time, + bool is_valid); + void set_connection_valid( + bool connection_valid, + std::chrono::steady_clock::time_point status_check_start_time, + std::chrono::steady_clock::time_point current_time); + void abort_connection(); + +private: + std::mutex mutex_; + + std::chrono::milliseconds failure_detection_time; + std::chrono::milliseconds failure_detection_interval; + int failure_detection_count; + + std::set node_keys; + DBC* connection_to_abort; + + std::chrono::steady_clock::time_point start_monitor_time; + std::chrono::steady_clock::time_point invalid_node_start_time; + int failure_count; + bool node_unhealthy; + std::atomic_bool active_context{ true }; + std::shared_ptr logger; + + std::string build_node_keys_str(); +}; + +#endif /* __MONITORCONNECTIONCONTEXT_H__ */ diff --git a/driver/monitor_service.cc b/driver/monitor_service.cc new file mode 100644 index 00000000..4d4cb3a2 --- /dev/null +++ b/driver/monitor_service.cc @@ -0,0 +1,145 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "monitor_service.h" + +#include "driver.h" + +MONITOR_SERVICE::MONITOR_SERVICE(bool enable_logging) { + this->thread_container = MONITOR_THREAD_CONTAINER::get_instance(); + if (enable_logging) + this->logger = init_log_file(); +} + +MONITOR_SERVICE::MONITOR_SERVICE( + std::shared_ptr monitor_thread_container, bool enable_logging) + : thread_container{std::move(monitor_thread_container)} { + + if (enable_logging) + this->logger = init_log_file(); +} + +std::shared_ptr MONITOR_SERVICE::start_monitoring( + DBC* dbc, + DataSource* ds, + std::set node_keys, + std::shared_ptr host, + std::chrono::milliseconds failure_detection_time, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds failure_detection_interval, + int failure_detection_count, + std::chrono::milliseconds disposal_time) { + + if (node_keys.empty()) { + auto msg = "[MONITOR_SERVICE] Parameter node_keys cannot be empty"; + MYLOG_TRACE(this->logger.get(), dbc ? dbc->id : 0, msg); + throw std::invalid_argument(msg); + } + + bool enable_logging = ds && ds->save_queries; + + std::shared_ptr monitor = this->thread_container->get_or_create_monitor( + node_keys, + std::move(host), + failure_detection_timeout, + disposal_time, + ds, + enable_logging); + + auto context = std::make_shared( + dbc, + node_keys, + failure_detection_time, + failure_detection_interval, + failure_detection_count, + enable_logging); + + monitor->start_monitoring(context); + this->thread_container->add_task(monitor, shared_from_this()); + + return context; +} + +void MONITOR_SERVICE::stop_monitoring(std::shared_ptr context) { + if (context == nullptr) { + MYLOG_TRACE( + this->logger.get(), 0, + "[MONITOR_SERVICE] Invalid context passed into stop_monitoring()"); + return; + } + + context->invalidate(); + + std::string node = this->thread_container->get_node(context->get_node_keys()); + if (node.empty()) { + MYLOG_TRACE( + this->logger.get(), context->get_dbc_id(), + "[MONITOR_SERVICE] Can not find node key from context"); + return; + } + + auto monitor = this->thread_container->get_monitor(node); + if (monitor != nullptr) { + monitor->stop_monitoring(context); + } +} + +void MONITOR_SERVICE::stop_monitoring_for_all_connections(std::set node_keys) { + std::string node = this->thread_container->get_node(node_keys); + if (node.empty()) { + MYLOG_TRACE( + this->logger.get(), 0, + "[MONITOR_SERVICE] Invalid node keys passed into stop_monitoring_for_all_connections(). " + "No existing monitor for the given set of node keys"); + return; + } + + auto monitor = this->thread_container->get_monitor(node); + if (monitor != nullptr) { + monitor->clear_contexts(); + this->thread_container->reset_resource(monitor); + } +} + +void MONITOR_SERVICE::notify_unused(const std::shared_ptr& monitor) const { + if (monitor == nullptr) { + MYLOG_TRACE( + this->logger.get(), 0, + "[MONITOR_SERVICE] Invalid monitor passed into notify_unused()"); + return; + } + + // Remove monitor from the maps + this->thread_container->release_resource(monitor); +} + +void MONITOR_SERVICE::release_resources() { + this->thread_container = nullptr; + MONITOR_THREAD_CONTAINER::release_instance(); +} diff --git a/driver/monitor_service.h b/driver/monitor_service.h new file mode 100644 index 00000000..1d772f4e --- /dev/null +++ b/driver/monitor_service.h @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MONITORSERVICE_H__ +#define __MONITORSERVICE_H__ + +#include "monitor_thread_container.h" + +class MONITOR_SERVICE : public std::enable_shared_from_this { +public: + MONITOR_SERVICE(bool enable_logging = false); + MONITOR_SERVICE( + std::shared_ptr monitor_thread_container, + bool enable_logging = false); + virtual ~MONITOR_SERVICE() = default; + + virtual std::shared_ptr start_monitoring( + DBC* dbc, + DataSource* ds, + std::set node_keys, + std::shared_ptr host, + std::chrono::milliseconds failure_detection_time, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds failure_detection_interval, + int failure_detection_count, + std::chrono::milliseconds disposal_time); + virtual void stop_monitoring(std::shared_ptr context); + virtual void stop_monitoring_for_all_connections(std::set node_keys); + void notify_unused(const std::shared_ptr& monitor) const; + void release_resources(); + +private: + std::shared_ptr thread_container; + std::shared_ptr logger; +}; + +#endif /* __MONITORSERVICE_H__ */ diff --git a/driver/monitor_thread_container.cc b/driver/monitor_thread_container.cc new file mode 100644 index 00000000..c6e47931 --- /dev/null +++ b/driver/monitor_thread_container.cc @@ -0,0 +1,226 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "monitor_thread_container.h" + +std::shared_ptr MONITOR_THREAD_CONTAINER::get_instance() { + if (singleton) { + return singleton; + } + + std::lock_guard guard(thread_container_singleton_mutex); + if (singleton) { + return singleton; + } + + singleton = std::shared_ptr(new MONITOR_THREAD_CONTAINER); + return singleton; +} + +void MONITOR_THREAD_CONTAINER::release_instance() { + if (!singleton) { + return; + } + + std::lock_guard guard(thread_container_singleton_mutex); + singleton->release_resources(); + singleton.reset(); +} + +std::string MONITOR_THREAD_CONTAINER::get_node(std::set node_keys) { + std::unique_lock lock(monitor_map_mutex); + if (!this->monitor_map.empty()) { + for (auto it = node_keys.begin(); it != node_keys.end(); ++it) { + std::string node = *it; + if (this->monitor_map.count(node) > 0) { + return node; + } + } + } + + return std::string{}; +} + +std::shared_ptr MONITOR_THREAD_CONTAINER::get_monitor(std::string node) { + std::unique_lock lock(monitor_map_mutex); + return this->monitor_map.count(node) > 0 ? this->monitor_map.at(node) : nullptr; +} + +std::shared_ptr MONITOR_THREAD_CONTAINER::get_or_create_monitor( + std::set node_keys, + std::shared_ptr host, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds disposal_time, + DataSource* ds, + bool enable_logging) { + + std::shared_ptr monitor; + + std::unique_lock lock(mutex_); + std::string node = this->get_node(node_keys); + if (!node.empty()) { + std::unique_lock lock(monitor_map_mutex); + monitor = this->monitor_map[node]; + } + else { + monitor = this->get_available_monitor(); + if (monitor == nullptr) { + monitor = this->create_monitor(std::move(host), failure_detection_timeout, disposal_time, ds, enable_logging); + } + } + + this->populate_monitor_map(node_keys, monitor); + + return monitor; +} + +void MONITOR_THREAD_CONTAINER::add_task(const std::shared_ptr& monitor, const std::shared_ptr& service) { + if (monitor == nullptr || service == nullptr) { + throw std::invalid_argument("Invalid parameters passed into MONITOR_THREAD_CONTAINER::add_task()"); + } + + std::unique_lock lock(task_map_mutex); + if (this->task_map.count(monitor) == 0) { + this->thread_pool.resize(this->thread_pool.size() + 1); + auto run_monitor = [monitor, service](int id) { monitor->run(service); }; + this->task_map[monitor] = this->thread_pool.push(run_monitor); + } +} + +void MONITOR_THREAD_CONTAINER::reset_resource(const std::shared_ptr& monitor) { + if (monitor == nullptr) { + return; + } + + this->remove_monitor_mapping(monitor); + + std::unique_lock lock(available_monitors_mutex); + this->available_monitors.push(monitor); +} + +void MONITOR_THREAD_CONTAINER::release_resource(std::shared_ptr monitor) { + if (monitor == nullptr) { + return; + } + + this->remove_monitor_mapping(monitor); + + { + std::unique_lock lock(task_map_mutex); + if (this->task_map.count(monitor) > 0) { + this->task_map.erase(monitor); + } + } + + if (this->thread_pool.n_idle() > 0) { + this->thread_pool.resize(this->thread_pool.size() - 1); + } +} + +void MONITOR_THREAD_CONTAINER::populate_monitor_map( + std::set node_keys, const std::shared_ptr& monitor) { + + for (auto it = node_keys.begin(); it != node_keys.end(); ++it) { + std::unique_lock lock(monitor_map_mutex); + this->monitor_map[*it] = monitor; + } +} + +void MONITOR_THREAD_CONTAINER::remove_monitor_mapping(const std::shared_ptr& monitor) { + std::unique_lock lock(monitor_map_mutex); + for (auto it = this->monitor_map.begin(); it != this->monitor_map.end();) { + std::string node = (*it).first; + if (this->monitor_map[node] == monitor) { + it = this->monitor_map.erase(it); + } + else { + ++it; + } + } +} + +std::shared_ptr MONITOR_THREAD_CONTAINER::get_available_monitor() { + std::unique_lock lock(available_monitors_mutex); + if (!this->available_monitors.empty()) { + std::shared_ptr available_monitor = this->available_monitors.front(); + this->available_monitors.pop(); + + if (!available_monitor->is_stopped()) { + return available_monitor; + } + + std::unique_lock lock(task_map_mutex); + if (this->task_map.count(available_monitor) > 0) { + available_monitor->stop(); + this->task_map.erase(available_monitor); + } + } + + return nullptr; +} + +std::shared_ptr MONITOR_THREAD_CONTAINER::create_monitor( + std::shared_ptr host, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds disposal_time, + DataSource* ds, + bool enable_logging) { + + return std::make_shared(host, failure_detection_timeout, disposal_time, ds, enable_logging); +} + +void MONITOR_THREAD_CONTAINER::release_resources() { + // Stop all monitors + { + std::unique_lock lock(task_map_mutex); + for (auto const& task_pair : task_map) { + auto monitor = task_pair.first; + monitor->stop(); + } + } + + // Wait for monitor threads to finish + this->thread_pool.stop(true); + + { + std::unique_lock lock(monitor_map_mutex); + this->monitor_map.clear(); + } + + { + std::unique_lock lock(task_map_mutex); + this->task_map.clear(); + } + + { + std::unique_lock lock(available_monitors_mutex); + std::queue> empty; + std::swap(available_monitors, empty); + } +} diff --git a/driver/monitor_thread_container.h b/driver/monitor_thread_container.h new file mode 100644 index 00000000..e855fee1 --- /dev/null +++ b/driver/monitor_thread_container.h @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MONITORTHREADCONTAINER_H__ +#define __MONITORTHREADCONTAINER_H__ + +#include "monitor.h" + +#include +#include +#include +#include + +class MONITOR_THREAD_CONTAINER { +public: + MONITOR_THREAD_CONTAINER(MONITOR_THREAD_CONTAINER const&) = delete; + MONITOR_THREAD_CONTAINER& operator=(MONITOR_THREAD_CONTAINER const&) = delete; + virtual ~MONITOR_THREAD_CONTAINER() = default; + std::string get_node(std::set node_keys); + std::shared_ptr get_monitor(std::string node); + std::shared_ptr get_or_create_monitor( + std::set node_keys, + std::shared_ptr host, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds disposal_time, + DataSource* ds, + bool enable_logging = false); + virtual void add_task(const std::shared_ptr& monitor, const std::shared_ptr& service); + void reset_resource(const std::shared_ptr& monitor); + void release_resource(std::shared_ptr monitor); + + static std::shared_ptr get_instance(); + static void release_instance(); + +protected: + MONITOR_THREAD_CONTAINER() = default; + void populate_monitor_map(std::set node_keys, const std::shared_ptr& monitor); + void remove_monitor_mapping(const std::shared_ptr& monitor); + std::shared_ptr get_available_monitor(); + virtual std::shared_ptr create_monitor( + std::shared_ptr host, + std::chrono::seconds failure_detection_timeout, + std::chrono::milliseconds disposal_time, + DataSource* ds, + bool enable_logging = false); + void release_resources(); + + std::map> monitor_map; + std::map, std::future> task_map; + std::queue> available_monitors; + std::mutex monitor_map_mutex; + std::mutex task_map_mutex; + std::mutex available_monitors_mutex; + ctpl::thread_pool thread_pool; + std::mutex mutex_; + +#ifdef UNIT_TEST_BUILD + // Allows for testing private methods + friend class TEST_UTILS; +#endif +}; + +static std::shared_ptr singleton; +static std::mutex thread_container_singleton_mutex; + +#endif /* __MONITORTHREADCONTAINER_H__ */ diff --git a/driver/my_prepared_stmt.cc b/driver/my_prepared_stmt.cc index ab898659..e140a157 100644 --- a/driver/my_prepared_stmt.cc +++ b/driver/my_prepared_stmt.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -27,7 +29,7 @@ // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA /** - @file ssps.c + @file my_prepared_stmt.cc @brief Functions to support use of Server Side Prepared Statements. */ @@ -65,7 +67,7 @@ static char * my_f_to_a(char * buf, size_t buf_size, double a) /* {{{ ssps_init() -I- */ void ssps_init(STMT *stmt) { - stmt->ssps= mysql_stmt_init(stmt->dbc->mysql); + stmt->ssps= stmt->dbc->mysql_proxy->stmt_init(); stmt->result_bind = 0; } @@ -100,7 +102,7 @@ BOOL ssps_get_out_params(STMT *stmt) MYSQL_ROW values= NULL; DESCREC *iprec, *aprec, *irrec; uint counter= 0; - int i, out_params; + int i, out_params = 0; /*Since OUT parameters can be completely different - we have to free current bind and bind new */ @@ -154,7 +156,7 @@ BOOL ssps_get_out_params(STMT *stmt) /* Making bit field look "normally" */ if (stmt->result_bind[counter].buffer_type == MYSQL_TYPE_BIT) { - MYSQL_FIELD *field= mysql_fetch_field_direct(stmt->result, counter); + MYSQL_FIELD *field= stmt->dbc->mysql_proxy->fetch_field_direct(stmt->result, counter); unsigned long long numeric; assert(field->type == MYSQL_TYPE_BIT); @@ -247,7 +249,7 @@ BOOL ssps_get_out_params(STMT *stmt) /* This MAGICAL fetch is required. If there are streams - it has to be after streams are all done, perhaps when stmt->out_params_state is changed from OPS_STREAMS_PENDING */ - mysql_stmt_fetch(stmt->ssps); + stmt->dbc->mysql_proxy->stmt_fetch(stmt->ssps); } return TRUE; @@ -264,7 +266,7 @@ int ssps_get_result(STMT *stmt) { if (!if_forward_cache(stmt)) { - return mysql_stmt_store_result(stmt->ssps); + return stmt->dbc->mysql_proxy->stmt_store_result(stmt->ssps); } else { @@ -345,7 +347,7 @@ void ssps_close(STMT *stmt) It can fail because the connection to the server is lost, which is still ok because the memory is freed anyway. */ - mysql_stmt_close(stmt->ssps); + stmt->dbc->mysql_proxy->stmt_close(stmt->ssps); stmt->ssps= NULL; } stmt->buf_set_pos(0); @@ -363,9 +365,9 @@ SQLRETURN ssps_fetch_chunk(STMT *stmt, char *dest, unsigned long dest_bytes, uns bind.is_null= &is_null; bind.error= &error; - if (mysql_stmt_fetch_column(stmt->ssps, &bind, stmt->getdata.column, stmt->getdata.src_offset)) + if (stmt->dbc->mysql_proxy->stmt_fetch_column(stmt->ssps, &bind, stmt->getdata.column, stmt->getdata.src_offset)) { - switch (mysql_stmt_errno(stmt->ssps)) + switch (stmt->dbc->mysql_proxy->stmt_errno(stmt->ssps)) { case CR_INVALID_PARAMETER_NO: /* Shouldn't really happen here*/ @@ -598,14 +600,14 @@ static MYSQL_ROW fetch_varlength_columns(STMT *stmt, MYSQL_ROW values) } if (reallocated_buffers) - mysql_stmt_fetch_column(stmt->ssps, &stmt->result_bind[i], i, 0); + stmt->dbc->mysql_proxy->stmt_fetch_column(stmt->ssps, &stmt->result_bind[i], i, 0); } } // Result buffers must be set again after reallocating if (reallocated_buffers) - mysql_stmt_bind_result(stmt->ssps, stmt->result_bind); + stmt->dbc->mysql_proxy->stmt_bind_result(stmt->ssps, stmt->result_bind); fill_ird_data_lengths(stmt->ird, stmt->result_bind[0].length, stmt->result->field_count); @@ -673,7 +675,7 @@ void STMT::free_reset_out_params() if (out_params_state == OPS_STREAMS_PENDING) { /* Magical out params fetch */ - mysql_stmt_fetch(ssps); + dbc->mysql_proxy->stmt_fetch(ssps); } out_params_state = OPS_UNKNOWN; apd->free_paramdata(); @@ -686,7 +688,7 @@ void STMT::free_reset_params() { if (ssps) { - mysql_stmt_reset(ssps); + dbc->mysql_proxy->stmt_reset(ssps); } /* remove all params and reset count to 0 (per spec) */ /* http://msdn2.microsoft.com/en-us/library/ms709284.aspx */ @@ -734,7 +736,7 @@ STMT::~STMT() if (ssps != NULL) { - mysql_stmt_close(ssps); + dbc->mysql_proxy->stmt_close(ssps); ssps = NULL; } @@ -765,19 +767,19 @@ SQLRETURN STMT::set_error(myodbc_errid errid, const char *errtext, SQLRETURN STMT::set_error(myodbc_errid errid) { - return set_error(errid, mysql_error(dbc->mysql), mysql_errno(dbc->mysql)); + return set_error(errid, dbc->mysql_proxy->error(), dbc->mysql_proxy->error_code()); } -SQLRETURN STMT::set_error(const char *state, const char *msg, +SQLRETURN STMT::set_error(const char *sqlstate, const char *msg, SQLINTEGER errcode) { - error = MYERROR(state, msg, errcode, dbc->st_error_prefix); + error = MYERROR(sqlstate, msg, errcode, dbc->st_error_prefix); return error.retcode; } SQLRETURN STMT::set_error(const char *state) { - return set_error(state, mysql_error(dbc->mysql), mysql_errno(dbc->mysql)); + return set_error(state, dbc->mysql_proxy->error(), dbc->mysql_proxy->error_code()); } @@ -862,7 +864,7 @@ long STMT::compute_cur_row(unsigned fFetchType, SQLLEN irow) case SQL_NO_DATA: throw MYERROR(SQL_NO_DATA_FOUND); case SQL_ERROR: - set_error(MYERR_S1000, mysql_error(dbc->mysql), 0); + set_error(MYERR_S1000, dbc->mysql_proxy->error(), 0); throw error; } } @@ -917,7 +919,7 @@ int STMT::ssps_bind_result() for (i= 0; i < num_fields; ++i) { - MYSQL_FIELD *field= mysql_fetch_field_direct(result, i); + MYSQL_FIELD *field= dbc->mysql_proxy->fetch_field_direct(result, i); st_buffer_size_type p= allocate_buffer_for_field(field, IS_PS_OUT_PARAMS(this)); @@ -944,11 +946,10 @@ int STMT::ssps_bind_result() } } - int rc = mysql_stmt_bind_result(ssps, result_bind); + int rc = dbc->mysql_proxy->stmt_bind_result(ssps, result_bind); if (rc) { - const char *err = mysql_stmt_error(ssps); - set_error("HY000", err, 0); + set_error("HY000", dbc->mysql_proxy->stmt_error(ssps), 0); } return rc; } @@ -1025,9 +1026,9 @@ SQLRETURN STMT::bind_query_attrs(bool use_ssps) MYSQL_BIND *bind = query_attr_bind.data(); const char** names = (const char**)query_attr_names.data(); - if (mysql_bind_param(dbc->mysql, rcount - param_count, - query_attr_bind.data(), - (const char**)query_attr_names.data())) + if (dbc->mysql_proxy->bind_param(rcount - param_count, + query_attr_bind.data(), + (const char**)query_attr_names.data())) { set_error("HY000"); return SQL_SUCCESS_WITH_INFO; @@ -1400,21 +1401,25 @@ T ssps_get_int64(STMT *stmt, ulong column_number, char *value, ulong length) SQLRETURN ssps_send_long_data(STMT *stmt, unsigned int param_number, const char *chunk, unsigned long length) { - if ( mysql_stmt_send_long_data(stmt->ssps, param_number, chunk, length)) + if ( stmt->dbc->mysql_proxy->stmt_send_long_data(stmt->ssps, param_number, chunk, length)) { - uint err= mysql_stmt_errno(stmt->ssps); + uint err = stmt->dbc->mysql_proxy->stmt_errno(stmt->ssps); switch (err) { case CR_INVALID_BUFFER_USE: /* We can fall back to assembling parameter's value on client */ return SQL_SUCCESS_WITH_INFO; case CR_SERVER_GONE_ERROR: - return stmt->set_error("08S01", mysql_stmt_error(stmt->ssps), err); + const char *error_code, *error_msg; + if (stmt->dbc->fh->trigger_failover_if_needed("08S01", error_code, error_msg)) + return stmt->set_error(error_code, error_msg, 0); + else + return stmt->set_error("08S01", stmt->dbc->mysql_proxy->stmt_error(stmt->ssps), err); case CR_COMMANDS_OUT_OF_SYNC: case CR_UNKNOWN_ERROR: - return stmt->set_error("HY000", mysql_stmt_error( stmt->ssps), err); + return stmt->set_error("HY000", stmt->dbc->mysql_proxy->stmt_error(stmt->ssps), err); case CR_OUT_OF_MEMORY: - return stmt->set_error("HY001", mysql_stmt_error(stmt->ssps), err); + return stmt->set_error("HY001", stmt->dbc->mysql_proxy->stmt_error(stmt->ssps), err); default: return stmt->set_error("HY000", "unhandled error from mysql_stmt_send_long_data", 0 ); } diff --git a/driver/my_stmt.cc b/driver/my_stmt.cc index 6987b7fc..6c4eccae 100644 --- a/driver/my_stmt.cc +++ b/driver/my_stmt.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -51,17 +53,17 @@ BOOL returned_result(STMT *stmt) MYSQL_RES *temp_res= NULL; if ((stmt->result != NULL) || - (temp_res= mysql_stmt_result_metadata(stmt->ssps)) != NULL) + (temp_res= stmt->dbc->mysql_proxy->stmt_result_metadata(stmt->ssps)) != NULL) { /* mysql_free_result checks for NULL, so we can always call it */ - mysql_free_result(temp_res); + stmt->dbc->mysql_proxy->free_result(temp_res); return TRUE; } return FALSE; } else { - return mysql_field_count(stmt->dbc->mysql) > 0 ; + return stmt->dbc->mysql_proxy->field_count() > 0; } } @@ -74,7 +76,7 @@ my_bool free_current_result(STMT *stmt) if (ssps_used(stmt)) { free_result_bind(stmt); - res= mysql_stmt_free_result(stmt->ssps); + res= stmt->dbc->mysql_proxy->stmt_free_result(stmt->ssps); } free_internal_result_buffers(stmt); /* We need to always free stmt->result because SSPS keep metadata there */ @@ -88,17 +90,16 @@ my_bool free_current_result(STMT *stmt) /* Name may be misleading, the idea is stmt - for directly executed statements, i.e using mysql_* part of api, ssps - prepared on server, using mysql_stmt */ -static -MYSQL_RES * stmt_get_result(STMT *stmt, BOOL force_use) +static MYSQL_RES* stmt_get_result(STMT *stmt, BOOL force_use) { /* We can't use USE_RESULT because SQLRowCount will fail in this case! */ if (if_forward_cache(stmt) || force_use) { - return mysql_use_result(stmt->dbc->mysql); + return stmt->dbc->mysql_proxy->use_result(); } else { - return mysql_store_result(stmt->dbc->mysql); + return stmt->dbc->mysql_proxy->store_result(); } } @@ -109,11 +110,11 @@ MYSQL_RES * get_result_metadata(STMT *stmt, BOOL force_use) { free_internal_result_buffers(stmt); /* just a precaution, mysql_free_result checks for NULL anywat */ - mysql_free_result(stmt->result); + stmt->dbc->mysql_proxy->free_result(stmt->result); if (ssps_used(stmt)) { - stmt->result= mysql_stmt_result_metadata(stmt->ssps); + stmt->result = stmt->dbc->mysql_proxy->stmt_result_metadata(stmt->ssps); } else { @@ -150,13 +151,13 @@ size_t STMT::field_count() { if (ssps) { - return mysql_stmt_field_count(ssps); + return dbc->mysql_proxy->stmt_field_count(ssps); } else { return result && result->field_count > 0 ? result->field_count : - mysql_field_count(dbc->mysql); + dbc->mysql_proxy->field_count(); } } @@ -165,12 +166,12 @@ my_ulonglong affected_rows(STMT *stmt) { if (ssps_used(stmt)) { - return mysql_stmt_affected_rows(stmt->ssps); + return stmt->dbc->mysql_proxy->stmt_affected_rows(stmt->ssps); } else { /* In some cases in c/odbc it cannot be used instead of mysql_num_rows */ - return mysql_affected_rows(stmt->dbc->mysql); + return stmt->dbc->mysql_proxy->affected_rows(); } } @@ -193,11 +194,11 @@ my_ulonglong num_rows(STMT *stmt) if (ssps_used(stmt)) { - return offset + mysql_stmt_num_rows(stmt->ssps); + return offset + stmt->dbc->mysql_proxy->stmt_num_rows(stmt->ssps); } else { - return offset + mysql_num_rows(stmt->result); + return offset + stmt->dbc->mysql_proxy->num_rows(stmt->result); } } @@ -215,7 +216,7 @@ MYSQL_ROW STMT::fetch_row(bool read_unbuffered) if (read_unbuffered || m_row_storage.eof()) { /* Reading results from network */ - err = mysql_stmt_fetch(ssps); + err = dbc->mysql_proxy->stmt_fetch(ssps); } else { @@ -226,8 +227,8 @@ MYSQL_ROW STMT::fetch_row(bool read_unbuffered) switch (err) { case 1: - set_error("HY000", mysql_stmt_error(ssps), - mysql_stmt_errno(ssps)); + set_error("HY000", dbc->mysql_proxy->stmt_error(ssps), + dbc->mysql_proxy->stmt_errno(ssps)); throw error; case MYSQL_NO_DATA: return nullptr; @@ -240,7 +241,7 @@ MYSQL_ROW STMT::fetch_row(bool read_unbuffered) } else { - return mysql_fetch_row(result); + return dbc->mysql_proxy->fetch_row(result); } } @@ -253,7 +254,7 @@ unsigned long* fetch_lengths(STMT *stmt) } else { - return mysql_fetch_lengths(stmt->result); + return stmt->dbc->mysql_proxy->fetch_lengths(stmt->result); } } @@ -262,11 +263,11 @@ MYSQL_ROW_OFFSET row_seek(STMT *stmt, MYSQL_ROW_OFFSET offset) { if (ssps_used(stmt)) { - return mysql_stmt_row_seek(stmt->ssps, offset); + return stmt->dbc->mysql_proxy->stmt_row_seek(stmt->ssps, offset); } else { - return mysql_row_seek(stmt->result, offset); + return stmt->dbc->mysql_proxy->row_seek(stmt->result, offset); } } @@ -275,11 +276,11 @@ void data_seek(STMT *stmt, my_ulonglong offset) { if (ssps_used(stmt)) { - mysql_stmt_data_seek(stmt->ssps, offset); + stmt->dbc->mysql_proxy->stmt_data_seek(stmt->ssps, offset); } else { - mysql_data_seek(stmt->result, offset); + stmt->dbc->mysql_proxy->data_seek(stmt->result, offset); } } @@ -288,11 +289,11 @@ MYSQL_ROW_OFFSET row_tell(STMT *stmt) { if (ssps_used(stmt)) { - return mysql_stmt_row_tell(stmt->ssps); + return stmt->dbc->mysql_proxy->stmt_row_tell(stmt->ssps); } else { - return mysql_row_tell(stmt->result); + return stmt->dbc->mysql_proxy->row_tell(stmt->result); } } @@ -303,11 +304,11 @@ int next_result(STMT *stmt) if (ssps_used(stmt)) { - return mysql_stmt_next_result(stmt->ssps); + return stmt->dbc->mysql_proxy->stmt_next_result(stmt->ssps); } else { - return mysql_next_result(stmt->dbc->mysql); + return stmt->dbc->mysql_proxy->next_result(); } } @@ -434,9 +435,9 @@ SQLRETURN prepare(STMT *stmt, char * query, SQLINTEGER query_length, actually parameter markers in it */ if (!stmt->dbc->ds->no_ssps && (PARAM_COUNT(stmt->query) || force_prepare) && !IS_BATCH(&stmt->query) - && preparable_on_server(&stmt->query, stmt->dbc->mysql->server_version)) + && preparable_on_server(&stmt->query, stmt->dbc->mysql_proxy->get_server_version())) { - MYLOG_QUERY(stmt, "Using prepared statement"); + MYLOG_STMT_TRACE(stmt, "Using prepared statement"); ssps_init(stmt); /* If the query is in the form of "WHERE CURRENT OF" - we do not need to prepare @@ -448,15 +449,15 @@ SQLRETURN prepare(STMT *stmt, char * query, SQLINTEGER query_length, if (reset_sql_limit) set_sql_select_limit(stmt->dbc, 0, false); - int prep_res = mysql_stmt_prepare(stmt->ssps, query, query_length); + int prep_res = stmt->dbc->mysql_proxy->stmt_prepare(stmt->ssps, query, query_length); if (prep_res) { - MYLOG_QUERY(stmt, mysql_error(stmt->dbc->mysql)); + MYLOG_STMT_TRACE(stmt, stmt->dbc->mysql_proxy->error()); stmt->set_error("HY000"); translate_error((char*)stmt->error.sqlstate.c_str(), MYERR_S1000, - mysql_errno(stmt->dbc->mysql)); + stmt->dbc->mysql_proxy->error_code()); return SQL_ERROR; } @@ -467,13 +468,13 @@ SQLRETURN prepare(STMT *stmt, char * query, SQLINTEGER query_length, /* make sure we free the result from the previous time */ if (stmt->result) { - mysql_free_result(stmt->result); + stmt->dbc->mysql_proxy->free_result(stmt->result); stmt->result = NULL; } /* Getting result metadata */ stmt->fake_result = false; // reset in case it was set before - if ((stmt->result= mysql_stmt_result_metadata(stmt->ssps))) + if ((stmt->result = stmt->dbc->mysql_proxy->stmt_result_metadata(stmt->ssps))) { /*stmt->state= ST_SS_PREPARED;*/ fix_result_types(stmt); @@ -487,8 +488,8 @@ SQLRETURN prepare(STMT *stmt, char * query, SQLINTEGER query_length, uint i; for (i= 0; i < stmt->param_count; ++i) { - DESCREC *aprec= desc_get_rec(stmt->apd, i, TRUE); - DESCREC *iprec= desc_get_rec(stmt->ipd, i, TRUE); + desc_get_rec(stmt->apd, i, TRUE); + desc_get_rec(stmt->ipd, i, TRUE); } } @@ -670,7 +671,7 @@ SQLRETURN scroller_prefetch(STMT * stmt) } } - MYLOG_QUERY(stmt, stmt->scroller.query); + MYLOG_STMT_TRACE(stmt, stmt->scroller.query); LOCK_DBC(stmt->dbc); @@ -695,9 +696,9 @@ BOOL scrollable(STMT * stmt, char * query, char * query_end) /* FOR UPDATE*/ { const char *before_token= query_end; - const char *last= mystr_get_prev_token(stmt->dbc->ansi_charset_info, - &before_token, - query); + mystr_get_prev_token(stmt->dbc->ansi_charset_info, + &before_token, + query); const char *prev= mystr_get_prev_token(stmt->dbc->ansi_charset_info, &before_token, query); diff --git a/driver/mylog.cc b/driver/mylog.cc new file mode 100644 index 00000000..54c22b73 --- /dev/null +++ b/driver/mylog.cc @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "mylog.h" + +#include +#include +#include + +#include "driver.h" + +#ifdef _WIN32 +#include +#else +#include +#endif + +void trace_print(FILE *file, unsigned long dbc_id, const char *fmt, ...) { + if (file && fmt) { + time_t now = time(nullptr); + char time_buf[256]; + strftime(time_buf, sizeof(time_buf), "%Y/%m/%d %X ", localtime(&now)); + + va_list args1; + va_start(args1, fmt); + va_list args2; + va_copy(args2, args1); + std::vector buf(1 + vsnprintf(nullptr, 0, fmt, args1)); + va_end(args1); + vsnprintf(buf.data(), buf.size(), fmt, args2); + va_end(args2); + +#ifdef _WIN32 + int pid; + pid = _getpid(); +#else + pid_t pid; + pid = getpid(); +#endif + + fprintf(file, "%s - Process ID %ld - DBC ID %lu - %s\n", time_buf, pid, + dbc_id, buf.data()); + fflush(file); + } +} + +std::shared_ptr init_log_file() { + if (log_file) + return log_file; + + std::lock_guard guard(log_file_mutex); + if (log_file) + return log_file; + + FILE *file; +#ifdef _WIN32 + char filename[MAX_PATH]; + size_t buffsize; + + getenv_s(&buffsize, filename, sizeof(filename), "TEMP"); + + if (buffsize) { + snprintf(filename + buffsize - 1, sizeof(filename) - buffsize + 1, "\\%s", DRIVER_LOG_FILE); + } else { + snprintf(filename, sizeof(filename), "c:\\%s", DRIVER_LOG_FILE); + } + + if (file = fopen(filename, "a+")) +#else + if (file = fopen(DRIVER_LOG_FILE, "a+")) +#endif + { + fprintf(file, "-- Driver logging\n"); + fprintf(file, "--\n"); + fprintf(file, "-- Driver name: %s Version: %s\n", DRIVER_NAME, + DRIVER_VERSION); +#ifdef HAVE_LOCALTIME_R + { + time_t now = time(nullptr); + struct tm start; + localtime_r(&now, &start); + + fprintf(file, "-- Timestamp: %02d%02d%02d %2d:%02d:%02d\n", + start.tm_year % 100, start.tm_mon + 1, start.tm_mday, + start.tm_hour, start.tm_min, start.tm_sec); + } +#endif /* HAVE_LOCALTIME_R */ + fprintf(file, "\n"); + log_file = std::shared_ptr(file, FILEDeleter()); + } + return log_file; +} + +void end_log_file() { + std::lock_guard guard(log_file_mutex); + if (log_file && log_file.use_count() == 1) { // static var + log_file.reset(); + } +} diff --git a/driver/mylog.h b/driver/mylog.h new file mode 100644 index 00000000..b352e772 --- /dev/null +++ b/driver/mylog.h @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MYLOG_H__ +#define __MYLOG_H__ + +#include +#include +#include +#include + +#define MYLOG_STMT_TRACE(A, B) \ + { \ + if ((A)->dbc->ds->save_queries) \ + trace_print((A)->dbc->log_file.get(), (A)->dbc->id, (const char *)B); \ + } + +#define MYLOG_DBC_TRACE(A, ...) \ + { trace_print((A)->log_file.get(), (A)->id, __VA_ARGS__); } + +#define MYLOG_TRACE(A, B, ...) \ + { \ + if ((A) != nullptr) trace_print((A), B, __VA_ARGS__); \ + } + +// stateless functor object for deleting FILE handle +struct FILEDeleter { + void operator()(FILE *file) { + if (file) { + fclose(file); + file = nullptr; + } + } +}; + +static std::shared_ptr log_file; +static std::mutex log_file_mutex; + +/* Functions used when debugging */ +std::shared_ptr init_log_file(); +void end_log_file(); +void trace_print(FILE *file, unsigned long dbc_id, const char *fmt, ...); + +#endif /* __MYLOG_H__ */ diff --git a/driver/mysql_proxy.cc b/driver/mysql_proxy.cc new file mode 100644 index 00000000..54622259 --- /dev/null +++ b/driver/mysql_proxy.cc @@ -0,0 +1,631 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "driver.h" + +#include + +namespace { + const char* RETRIEVE_HOST_PORT_SQL = "SELECT CONCAT(@@hostname, ':', @@port)"; + const auto SOCKET_CLOSE_DELAY = std::chrono::milliseconds(100); +} + +MYSQL_PROXY::MYSQL_PROXY(DBC* dbc, DataSource* ds) + : MYSQL_PROXY(dbc, ds, std::make_shared(ds && ds->save_queries)) {} + +MYSQL_PROXY::MYSQL_PROXY(DBC* dbc, DataSource* ds, std::shared_ptr monitor_service) : dbc{dbc}, ds{ds} { + if (!this->dbc) { + throw std::runtime_error("DBC cannot be null."); + } + + if (!this->ds) { + throw std::runtime_error("DataSource cannot be null."); + } + + if (this->ds->enable_failure_detection) { + this->monitor_service = std::move(monitor_service); + } + + this->host = get_host_info_from_ds(ds); + generate_node_keys(); +} + +MYSQL_PROXY::~MYSQL_PROXY() { + if (this->mysql) { + close(); + } +} + +void MYSQL_PROXY::delete_ds() { + if (ds) { + ds_delete(ds); + ds = nullptr; + } +} + +uint64_t MYSQL_PROXY::num_rows(MYSQL_RES* res) { + return mysql_num_rows(res); +} + +unsigned int MYSQL_PROXY::num_fields(MYSQL_RES* res) { + return mysql_num_fields(res); +} + +MYSQL_FIELD* MYSQL_PROXY::fetch_field_direct(MYSQL_RES* res, unsigned int fieldnr) { + return mysql_fetch_field_direct(res, fieldnr); +} + +MYSQL_ROW_OFFSET MYSQL_PROXY::row_tell(MYSQL_RES* res) { + return mysql_row_tell(res); +} + +unsigned int MYSQL_PROXY::field_count() { + return mysql_field_count(mysql); +} + +uint64_t MYSQL_PROXY::affected_rows() { + return mysql_affected_rows(mysql); +} + +unsigned int MYSQL_PROXY::error_code() { + return mysql_errno(mysql); +} + +const char* MYSQL_PROXY::error() { + return mysql_error(mysql); +} + +const char* MYSQL_PROXY::sqlstate() { + return mysql_sqlstate(mysql); +} + +unsigned long MYSQL_PROXY::thread_id() { + return mysql_thread_id(mysql); +} + +int MYSQL_PROXY::set_character_set(const char* csname) { + const auto context = start_monitoring(); + const int ret = mysql_set_character_set(mysql, csname); + stop_monitoring(context); + return ret; +} + +void MYSQL_PROXY::init() { + const auto context = start_monitoring(); + this->mysql = mysql_init(nullptr); + stop_monitoring(context); +} + +bool MYSQL_PROXY::ssl_set(const char* key, const char* cert, const char* ca, const char* capath, const char* cipher) { + const auto context = start_monitoring(); + const bool ret = mysql_ssl_set(mysql, key, cert, ca, capath, cipher); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::change_user(const char* user, const char* passwd, const char* db) { + const auto context = start_monitoring(); + const bool ret = mysql_change_user(mysql, user, passwd, db); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::real_connect( + const char* host, const char* user, const char* passwd, + const char* db, unsigned int port, const char* unix_socket, + unsigned long clientflag) { + + const auto context = start_monitoring(); + const MYSQL* new_mysql = mysql_real_connect(mysql, host, user, passwd, db, port, unix_socket, clientflag); + stop_monitoring(context); + return new_mysql != nullptr; +} + +int MYSQL_PROXY::select_db(const char* db) { + const auto context = start_monitoring(); + const int ret = mysql_select_db(mysql, db); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::query(const char* q) { + const auto context = start_monitoring(); + const int ret = mysql_query(mysql, q); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::real_query(const char* q, unsigned long length) { + const auto context = start_monitoring(); + const int ret = mysql_real_query(mysql, q, length); + stop_monitoring(context); + return ret; +} + +MYSQL_RES* MYSQL_PROXY::store_result() { + const auto context = start_monitoring(); + MYSQL_RES* ret = mysql_store_result(mysql); + stop_monitoring(context); + return ret; +} + +MYSQL_RES* MYSQL_PROXY::use_result() { + const auto context = start_monitoring(); + MYSQL_RES* ret = mysql_use_result(mysql); + stop_monitoring(context); + return ret; +} + +struct CHARSET_INFO* MYSQL_PROXY::get_character_set() const { + return this->mysql->charset; +} + +void MYSQL_PROXY::get_character_set_info(MY_CHARSET_INFO* charset) { + mysql_get_character_set_info(mysql, charset); +} + +bool MYSQL_PROXY::autocommit(bool auto_mode) { + const auto context = start_monitoring(); + const bool ret = mysql_autocommit(mysql, auto_mode); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::next_result() { + const auto context = start_monitoring(); + const int ret = mysql_next_result(mysql); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_next_result(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_next_result(stmt); + stop_monitoring(context); + return ret; +} + +void MYSQL_PROXY::close() { + mysql_close(mysql); + mysql = nullptr; +} + +bool MYSQL_PROXY::real_connect_dns_srv( + const char* dns_srv_name, const char* user, + const char* passwd, const char* db, unsigned long client_flag) { + + const auto context = start_monitoring(); + const MYSQL* new_mysql = mysql_real_connect_dns_srv(mysql, dns_srv_name, user, passwd, db, client_flag); + stop_monitoring(context); + return new_mysql != nullptr; +} + +int MYSQL_PROXY::ping() { + const auto context = start_monitoring(); + const int ret = mysql_ping(mysql); + stop_monitoring(context); + return ret; +} + +unsigned long MYSQL_PROXY::get_client_version(void) { + return mysql_get_client_version(); +} + +int MYSQL_PROXY::get_option(mysql_option option, const void* arg) { + return mysql_get_option(mysql, option, arg); +} + +int MYSQL_PROXY::options4(mysql_option option, const void* arg1, const void* arg2) { + const auto context = start_monitoring(); + const int ret = mysql_options4(mysql, option, arg1, arg2); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::options(mysql_option option, const void* arg) { + const auto context = start_monitoring(); + const int ret = mysql_options(mysql, option, arg); + stop_monitoring(context); + return ret; +} + +void MYSQL_PROXY::free_result(MYSQL_RES* result) { + const auto context = start_monitoring(); + mysql_free_result(result); + stop_monitoring(context); +} + +void MYSQL_PROXY::data_seek(MYSQL_RES* result, uint64_t offset) { + mysql_data_seek(result, offset); +} + +MYSQL_ROW_OFFSET MYSQL_PROXY::row_seek(MYSQL_RES* result, MYSQL_ROW_OFFSET offset) { + return mysql_row_seek(result, offset); +} + +MYSQL_FIELD_OFFSET MYSQL_PROXY::field_seek(MYSQL_RES* result, MYSQL_FIELD_OFFSET offset) { + return mysql_field_seek(result, offset); +} + +MYSQL_ROW MYSQL_PROXY::fetch_row(MYSQL_RES* result) { + const auto context = start_monitoring(); + const MYSQL_ROW ret = mysql_fetch_row(result); + stop_monitoring(context); + return ret; +} + +unsigned long* MYSQL_PROXY::fetch_lengths(MYSQL_RES* result) { + return mysql_fetch_lengths(result); +} + +MYSQL_FIELD* MYSQL_PROXY::fetch_field(MYSQL_RES* result) { + return mysql_fetch_field(result); +} + +MYSQL_RES* MYSQL_PROXY::list_fields(const char* table, const char* wild) { + const auto context = start_monitoring(); + MYSQL_RES* ret = mysql_list_fields(mysql, table, wild); + stop_monitoring(context); + return ret; +} + +unsigned long MYSQL_PROXY::real_escape_string(char* to, const char* from, unsigned long length) { + const auto context = start_monitoring(); + const unsigned long ret = mysql_real_escape_string(mysql, to, from, length); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::bind_param(unsigned n_params, MYSQL_BIND* binds, const char** names) { + const auto context = start_monitoring(); + const bool ret = mysql_bind_param(mysql, n_params, binds, names); + stop_monitoring(context); + return ret; +} + +MYSQL_STMT* MYSQL_PROXY::stmt_init() { + const auto context = start_monitoring(); + MYSQL_STMT* ret = mysql_stmt_init(mysql); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_prepare(MYSQL_STMT* stmt, const char* query, unsigned long length) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_prepare(stmt, query, length); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_execute(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_execute(stmt); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_fetch(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_fetch(stmt); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_fetch_column(MYSQL_STMT* stmt, MYSQL_BIND* bind_arg, unsigned int column, unsigned long offset) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_fetch_column(stmt, bind_arg, column, offset); + stop_monitoring(context); + return ret; +} + +int MYSQL_PROXY::stmt_store_result(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const int ret = mysql_stmt_store_result(stmt); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_bind_param(MYSQL_STMT* stmt, MYSQL_BIND* bnd) { + const auto context = start_monitoring(); + const bool ret = mysql_stmt_bind_param(stmt, bnd); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_bind_result(MYSQL_STMT* stmt, MYSQL_BIND* bnd) { + const auto context = start_monitoring(); + const bool ret = mysql_stmt_bind_result(stmt, bnd); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_close(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const bool ret = mysql_stmt_close(stmt); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_reset(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const bool ret = mysql_stmt_reset(stmt); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_free_result(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + const bool ret = mysql_stmt_free_result(stmt); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::stmt_send_long_data(MYSQL_STMT* stmt, unsigned int param_number, const char* data, + unsigned long length) { + + const auto context = start_monitoring(); + const bool ret = mysql_stmt_send_long_data(stmt, param_number, data, length); + stop_monitoring(context); + return ret; +} + +MYSQL_RES* MYSQL_PROXY::stmt_result_metadata(MYSQL_STMT* stmt) { + const auto context = start_monitoring(); + MYSQL_RES* ret = mysql_stmt_result_metadata(stmt); + stop_monitoring(context); + return ret; +} + +unsigned int MYSQL_PROXY::stmt_errno(MYSQL_STMT* stmt) { + return mysql_stmt_errno(stmt); +} + +const char* MYSQL_PROXY::stmt_error(MYSQL_STMT* stmt) { + return mysql_stmt_error(stmt); +} + +MYSQL_ROW_OFFSET MYSQL_PROXY::stmt_row_seek(MYSQL_STMT* stmt, MYSQL_ROW_OFFSET offset) { + return mysql_stmt_row_seek(stmt, offset); +} + +MYSQL_ROW_OFFSET MYSQL_PROXY::stmt_row_tell(MYSQL_STMT* stmt) { + return mysql_stmt_row_tell(stmt); +} + +void MYSQL_PROXY::stmt_data_seek(MYSQL_STMT* stmt, uint64_t offset) { + mysql_stmt_data_seek(stmt, offset); +} + +uint64_t MYSQL_PROXY::stmt_num_rows(MYSQL_STMT* stmt) { + return mysql_stmt_num_rows(stmt); +} + +uint64_t MYSQL_PROXY::stmt_affected_rows(MYSQL_STMT* stmt) { + return mysql_stmt_affected_rows(stmt); +} + +unsigned int MYSQL_PROXY::stmt_field_count(MYSQL_STMT* stmt) { + return mysql_stmt_field_count(stmt); +} + +st_mysql_client_plugin* MYSQL_PROXY::client_find_plugin(const char* name, int type) { + const auto context = start_monitoring(); + st_mysql_client_plugin* ret = mysql_client_find_plugin(mysql, name, type); + stop_monitoring(context); + return ret; +} + +bool MYSQL_PROXY::is_connected() { + return this->mysql != nullptr && this->mysql->net.vio; +} + +void MYSQL_PROXY::set_last_error_code(unsigned int error_code) { + this->mysql->net.last_errno = error_code; +} + +char* MYSQL_PROXY::get_last_error() const { + return this->mysql->net.last_error; +} + +unsigned int MYSQL_PROXY::get_last_error_code() const { + return this->mysql->net.last_errno; +} + +char* MYSQL_PROXY::get_sqlstate() const { + return this->mysql->net.sqlstate; +} + +char* MYSQL_PROXY::get_server_version() const { + return this->mysql->server_version; +} + +uint64_t MYSQL_PROXY::get_affected_rows() const { + return this->mysql->affected_rows; +} + +void MYSQL_PROXY::set_affected_rows(uint64_t num_rows) { + this->mysql->affected_rows = num_rows; +} + +char* MYSQL_PROXY::get_host_info() const { + return this->mysql->host_info; +} + +std::string MYSQL_PROXY::get_host() { + return (this->mysql && this->mysql->host) ? this->mysql->host : this->host->get_host(); +} + +unsigned int MYSQL_PROXY::get_port() { + return (this->mysql) ? this->mysql->port : this->host->get_port(); +} + +unsigned long MYSQL_PROXY::get_max_packet() const { + return this->mysql->net.max_packet; +} + +unsigned long MYSQL_PROXY::get_server_capabilities() const { + return this->mysql->server_capabilities; +} + +unsigned int MYSQL_PROXY::get_server_status() const { + return this->mysql->server_status; +} + +void MYSQL_PROXY::set_connection(MYSQL_PROXY* mysql_proxy) { + close(); + this->mysql = mysql_proxy->mysql; + mysql_proxy->mysql = nullptr; + + ds_delete(mysql_proxy->ds); + delete mysql_proxy; + + if (monitor_service != nullptr && !node_keys.empty()) { + monitor_service->stop_monitoring_for_all_connections(node_keys); + } + generate_node_keys(); +} + +void MYSQL_PROXY::close_socket() { + MYLOG_DBC_TRACE(dbc, "Closing socket"); + int ret = 0; + if (mysql->net.fd != INVALID_SOCKET && (ret = shutdown(mysql->net.fd, SHUT_RDWR))) { + MYLOG_DBC_TRACE(dbc, "shutdown() with return code: %d, error message: %s,", ret, strerror(socket_errno)); + } + // Yield to main thread to handle socket shutdown + std::this_thread::sleep_for(SOCKET_CLOSE_DELAY); + if (mysql->net.fd != INVALID_SOCKET && (ret = ::closesocket(mysql->net.fd))) { + MYLOG_DBC_TRACE(dbc, "closesocket() with return code: %d, error message: %s,", ret, strerror(socket_errno)); + } +} + +std::shared_ptr MYSQL_PROXY::start_monitoring() { + if (!ds || !ds->enable_failure_detection) { + return nullptr; + } + + + auto failure_detection_timeout = ds->failure_detection_timeout; + // Use network timeout defined if failure detection timeout is not set + if (failure_detection_timeout == 0) { + failure_detection_timeout = ds->network_timeout == 0 ? failure_detection_timeout_default : ds->network_timeout; + } + + return monitor_service->start_monitoring( + dbc, + ds, + node_keys, + std::make_shared(get_host(), get_port()), + std::chrono::milliseconds{ds->failure_detection_time}, + std::chrono::seconds{failure_detection_timeout}, + std::chrono::milliseconds{ds->failure_detection_interval}, + ds->failure_detection_count, + std::chrono::milliseconds{ds->monitor_disposal_time}); +} + +void MYSQL_PROXY::stop_monitoring(std::shared_ptr context) { + if (!ds ||!ds->enable_failure_detection || context == nullptr) { + return; + } + monitor_service->stop_monitoring(context); + if (context->is_node_unhealthy() && is_connected()) { + close_socket(); + } +} + +void MYSQL_PROXY::generate_node_keys() { + node_keys.clear(); + node_keys.insert(std::string(get_host()) + ":" + std::to_string(get_port())); + + if (is_connected()) { + // Temporarily turn off failure detection if on + const auto failure_detection_old_state = ds->enable_failure_detection; + ds->enable_failure_detection = false; + + const auto error = query(RETRIEVE_HOST_PORT_SQL); + if (error == 0) { + MYSQL_RES* result = store_result(); + MYSQL_ROW row; + while ((row = fetch_row(result))) { + node_keys.insert(std::string(row[0])); + } + free_result(result); + } + + ds->enable_failure_detection = failure_detection_old_state; + } +} + +MYSQL_MONITOR_PROXY::MYSQL_MONITOR_PROXY(DataSource* ds) { + this->ds = ds_new(); + ds_copy(this->ds, ds); +} + +MYSQL_MONITOR_PROXY::~MYSQL_MONITOR_PROXY() { + if (this->mysql) { + mysql_close(this->mysql); + } + if (this->ds) { + ds_delete(this->ds); + } +} + +void MYSQL_MONITOR_PROXY::init() { + this->mysql = mysql_init(nullptr); +} + +int MYSQL_MONITOR_PROXY::ping() { + return mysql_ping(mysql); +} + +int MYSQL_MONITOR_PROXY::options(enum mysql_option option, const void* arg) { + return mysql_options(mysql, option, arg); +} + +bool MYSQL_MONITOR_PROXY::connect() { + if (!ds) + return false; + + const auto host = get_host_info_from_ds(ds); + + return mysql_real_connect(mysql, host->get_host().c_str(), ds_get_utf8attr(ds->uid, &ds->uid8), + ds_get_utf8attr(ds->pwd, &ds->pwd8), nullptr, host->get_port(), + ds_get_utf8attr(ds->socket, &ds->socket8), 0) != nullptr; +} + +bool MYSQL_MONITOR_PROXY::is_connected() { + return this->mysql != nullptr && this->mysql->net.vio; +} + +const char* MYSQL_MONITOR_PROXY::error() { + return mysql_error(mysql); } + +void MYSQL_MONITOR_PROXY::close() { + mysql_close(mysql); + mysql= nullptr; +} diff --git a/driver/mysql_proxy.h b/driver/mysql_proxy.h new file mode 100644 index 00000000..20c9de2c --- /dev/null +++ b/driver/mysql_proxy.h @@ -0,0 +1,196 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __MYSQL_PROXY__ +#define __MYSQL_PROXY__ + +#include + +#include "monitor_service.h" + +struct DBC; +struct DataSource; + +class MYSQL_PROXY { +public: + MYSQL_PROXY(DBC* dbc, DataSource* ds); + MYSQL_PROXY(DBC* dbc, DataSource* ds, std::shared_ptr monitor_service); + virtual ~MYSQL_PROXY(); + + void delete_ds(); + uint64_t num_rows(MYSQL_RES* res); + unsigned int num_fields(MYSQL_RES* res); + MYSQL_FIELD* fetch_field_direct(MYSQL_RES* res, unsigned int fieldnr); + MYSQL_ROW_OFFSET row_tell(MYSQL_RES* res); + + unsigned int field_count(); + uint64_t affected_rows(); + unsigned int error_code(); + const char* error(); + const char* sqlstate(); + unsigned long thread_id(); + int set_character_set(const char* csname); + + void init(); + bool ssl_set(const char* key, const char* cert, const char* ca, + const char* capath, const char* cipher); + bool change_user(const char* user, const char* passwd, + const char* db); + bool real_connect(const char* host, const char* user, + const char* passwd, const char* db, unsigned int port, + const char* unix_socket, unsigned long clientflag); + int select_db(const char* db); + virtual int query(const char* q); + int real_query(const char* q, unsigned long length); + virtual MYSQL_RES* store_result(); + MYSQL_RES* use_result(); + struct CHARSET_INFO* get_character_set() const; + void get_character_set_info(MY_CHARSET_INFO* charset); + + virtual int ping(); + static unsigned long get_client_version(void); + virtual int options(enum mysql_option option, const void* arg); + int options4(enum mysql_option option, const void* arg1, + const void* arg2); + int get_option(enum mysql_option option, const void* arg); + virtual void free_result(MYSQL_RES* result); + void data_seek(MYSQL_RES* result, uint64_t offset); + MYSQL_ROW_OFFSET row_seek(MYSQL_RES* result, MYSQL_ROW_OFFSET offset); + MYSQL_FIELD_OFFSET field_seek(MYSQL_RES* result, MYSQL_FIELD_OFFSET offset); + virtual MYSQL_ROW fetch_row(MYSQL_RES* result); + + unsigned long* fetch_lengths(MYSQL_RES* result); + MYSQL_FIELD* fetch_field(MYSQL_RES* result); + MYSQL_RES* list_fields(const char* table, const char* wild); + unsigned long real_escape_string(char* to, const char* from, + unsigned long length); + + bool bind_param(unsigned n_params, MYSQL_BIND* binds, + const char** names); + + MYSQL_STMT* stmt_init(); + int stmt_prepare(MYSQL_STMT* stmt, const char* query, unsigned long length); + int stmt_execute(MYSQL_STMT* stmt); + int stmt_fetch(MYSQL_STMT* stmt); + int stmt_fetch_column(MYSQL_STMT* stmt, MYSQL_BIND* bind_arg, + unsigned int column, unsigned long offset); + int stmt_store_result(MYSQL_STMT* stmt); + unsigned long stmt_param_count(MYSQL_STMT* stmt); + bool stmt_bind_param(MYSQL_STMT* stmt, MYSQL_BIND* bnd); + bool stmt_bind_result(MYSQL_STMT* stmt, MYSQL_BIND* bnd); + bool stmt_close(MYSQL_STMT* stmt); + bool stmt_reset(MYSQL_STMT* stmt); + bool stmt_free_result(MYSQL_STMT* stmt); + bool stmt_send_long_data(MYSQL_STMT* stmt, unsigned int param_number, + const char* data, unsigned long length); + MYSQL_RES* stmt_result_metadata(MYSQL_STMT* stmt); + unsigned int stmt_errno(MYSQL_STMT* stmt); + const char* stmt_error(MYSQL_STMT* stmt); + MYSQL_ROW_OFFSET stmt_row_seek(MYSQL_STMT* stmt, MYSQL_ROW_OFFSET offset); + MYSQL_ROW_OFFSET stmt_row_tell(MYSQL_STMT* stmt); + void stmt_data_seek(MYSQL_STMT* stmt, uint64_t offset); + uint64_t stmt_num_rows(MYSQL_STMT* stmt); + uint64_t stmt_affected_rows(MYSQL_STMT* stmt); + unsigned int stmt_field_count(MYSQL_STMT* stmt); + + bool autocommit(bool auto_mode); + int next_result(); + int stmt_next_result(MYSQL_STMT* stmt); + void close(); + + bool real_connect_dns_srv(const char* dns_srv_name, + const char* user, const char* passwd, + const char* db, unsigned long client_flag); + struct st_mysql_client_plugin* client_find_plugin( + const char* name, int type); + + virtual bool is_connected(); + + void set_last_error_code(unsigned int error_code); + + char* get_last_error() const; + + unsigned int get_last_error_code() const; + + char* get_sqlstate() const; + + char* get_server_version() const; + + uint64_t get_affected_rows() const; + + void set_affected_rows(uint64_t num_rows); + + char* get_host_info() const; + + std::string get_host(); + + unsigned int get_port(); + + unsigned long get_max_packet() const; + + unsigned long get_server_capabilities() const; + + unsigned int get_server_status() const; + + void set_connection(MYSQL_PROXY* mysql_proxy); + + virtual void close_socket(); + +protected: + DBC* dbc = nullptr; + DataSource* ds = nullptr; + MYSQL* mysql = nullptr; + std::shared_ptr monitor_service = nullptr; + std::shared_ptr host = nullptr; + std::set node_keys; + + std::shared_ptr start_monitoring(); + void stop_monitoring(std::shared_ptr context); + void generate_node_keys(); +}; + +class MYSQL_MONITOR_PROXY { +public: + MYSQL_MONITOR_PROXY(DataSource* ds); + virtual ~MYSQL_MONITOR_PROXY(); + + virtual void init(); + virtual int ping(); + virtual int options(enum mysql_option option, const void* arg); + virtual bool connect(); + virtual bool is_connected(); + virtual const char* error(); + virtual void close(); + +private: + DataSource* ds = nullptr; + MYSQL* mysql = nullptr; +}; + +#endif /* __MYSQL_PROXY__ */ diff --git a/driver/myutil.h b/driver/myutil.h index eb5c6e7e..f5f2c614 100755 --- a/driver/myutil.h +++ b/driver/myutil.h @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -45,19 +47,12 @@ #define if_forward_cache(st) ((st)->stmt_options.cursor_type == SQL_CURSOR_FORWARD_ONLY && \ (st)->dbc->ds->dont_cache_result) -#define is_connected(dbc) ((dbc)->mysql && (dbc)->mysql->net.vio) -#define trans_supported(db) ((db)->mysql->server_capabilities & CLIENT_TRANSACTIONS) -#define autocommit_on(db) ((db)->mysql->server_status & SERVER_STATUS_AUTOCOMMIT) -#define is_no_backslashes_escape_mode(db) ((db)->mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) +#define trans_supported(db) ((db)->mysql_proxy->get_server_capabilities() & CLIENT_TRANSACTIONS) +#define autocommit_on(db) ((db)->mysql_proxy->get_server_status() & SERVER_STATUS_AUTOCOMMIT) +#define is_no_backslashes_escape_mode(db) ((db)->mysql_proxy->get_server_status() & SERVER_STATUS_NO_BACKSLASH_ESCAPES) #define reset_ptr(x) {if (x) x= 0;} #define digit(A) ((int) (A - '0')) -#define MYLOG_QUERY(A,B) {if ((A)->dbc->ds->save_queries) \ - query_print((A)->dbc->query_log,(char*) B);} - -#define MYLOG_DBC_QUERY(A,B) {if((A)->ds->save_queries) \ - query_print((A)->query_log,(char*) B);} - /* A few character sets we care about. */ #define ASCII_CHARSET_NUMBER 11 #define BINARY_CHARSET_NUMBER 63 @@ -189,7 +184,7 @@ SQLRETURN set_desc_error (DESC *desc, char *state, const char *message, uint errcode); SQLRETURN handle_connection_error (STMT *stmt); my_bool is_connection_lost (uint errcode); -void set_mem_error (MYSQL *mysql); +void set_mem_error (MYSQL_PROXY *mysql_proxy); void translate_error (char *save_state, myodbc_errid errid, uint mysql_err); SQLSMALLINT get_sql_data_type_from_str(const char *mysql_type_name); @@ -198,12 +193,12 @@ SQLSMALLINT compute_sql_data_type(STMT *stmt, SQLSMALLINT sql_type, SQLSMALLINT get_sql_data_type (STMT *stmt, MYSQL_FIELD *field, char *buff); SQLULEN get_column_size (STMT *stmt, MYSQL_FIELD *field); SQLULEN get_column_size_from_str (STMT *stmt, const char *size_str); -SQLULEN fill_column_size_buff (char *buff, STMT *stmt, MYSQL_FIELD *field); +SQLULEN fill_column_size_buff (char *buff, size_t buff_size, STMT *stmt, MYSQL_FIELD *field); SQLSMALLINT get_decimal_digits (STMT *stmt, MYSQL_FIELD *field); SQLLEN get_transfer_octet_length (STMT *stmt, MYSQL_FIELD *field); SQLLEN fill_transfer_oct_len_buff (char *buff, STMT *stmt, MYSQL_FIELD *field); SQLLEN get_display_size (STMT *stmt, MYSQL_FIELD *field); -SQLLEN fill_display_size_buff (char *buff, STMT *stmt, MYSQL_FIELD *field); +SQLLEN fill_display_size_buff (char *buff, size_t buff_size, STMT *stmt, MYSQL_FIELD *field); SQLSMALLINT get_dticode_from_concise_type (SQLSMALLINT concise_type); SQLSMALLINT get_concise_type_from_datetime_code (SQLSMALLINT dticode); SQLSMALLINT get_concise_type_from_interval_code (SQLSMALLINT dticode); @@ -267,14 +262,14 @@ void myodbc_init (void); void myodbc_ov_init (SQLINTEGER odbc_version); void myodbc_sqlstate2_init (void); void myodbc_sqlstate3_init (void); -int check_if_server_is_alive (DBC *dbc); +bool is_server_alive (DBC *dbc); bool myodbc_append_quoted_name_std(std::string &str, const char *name); SQLRETURN set_handle_error (SQLSMALLINT HandleType, SQLHANDLE handle, myodbc_errid errid, const char *errtext, SQLINTEGER errcode); SQLRETURN set_conn_error(DBC *dbc,myodbc_errid errid, const char *errtext, - SQLINTEGER errcode); + SQLINTEGER errcode, char* prefix = MYODBC_ERROR_PREFIX); SQLRETURN set_env_error (ENV * env,myodbc_errid errid, const char *errtext, SQLINTEGER errcode); SQLRETURN copy_str_data (SQLSMALLINT HandleType, SQLHANDLE Handle, @@ -323,11 +318,6 @@ void *ptr_offset_adjust (void *ptr, SQLULEN *bind_offset, void free_internal_result_buffers(STMT *stmt); -/* Functions used when debugging */ -void query_print (FILE *log_file,char *query); -FILE *init_query_log (void); -void end_query_log (FILE *query_log); - enum enum_field_types map_sql2mysql_type(SQLSMALLINT sql_type); /* proc_* functions - used to parse prcedures headers in SQLProcedureColumns */ @@ -339,10 +329,10 @@ SQLUINTEGER proc_get_param_size (SQLCHAR *ptype, int len, int sql_type_index, SQLSMALLINT *dec); SQLLEN proc_get_param_octet_len (STMT *stmt, int sql_type_index, SQLULEN col_size, SQLSMALLINT decimal_digits, - unsigned int flags, char * str_buff); + unsigned int flags, char * str_buff, size_t buff_size); SQLLEN proc_get_param_col_len (STMT *stmt, int sql_type_index, SQLULEN col_size, SQLSMALLINT decimal_digits, unsigned int flags, - char * str_buff); + char * str_buff, size_t buff_size); int proc_get_param_sql_type_index (const char*ptype, int len); SQLTypeMap *proc_get_param_map_by_index (int index); char * proc_param_next_token (char *str, char *str_end); @@ -383,7 +373,7 @@ unsigned long long binary2ull(char* src, uint64 srcLen); void fill_ird_data_lengths (DESC *ird, ulong *lengths, uint fields); /* Functions to work with prepared and regular statements */ -#define IS_PS_OUT_PARAMS(_stmt) ((_stmt)->dbc->mysql->server_status & SERVER_PS_OUT_PARAMS) +#define IS_PS_OUT_PARAMS(_stmt) ((_stmt)->dbc->mysql_proxy->get_server_status() & SERVER_PS_OUT_PARAMS) /* my_stmt.c */ BOOL ssps_used (STMT *stmt); BOOL returned_result (STMT *stmt); @@ -432,7 +422,7 @@ void stmt_result_free(STMT * stmt) x_free(stmt->result); } else - mysql_free_result(stmt->result); + stmt->dbc->mysql_proxy->free_result(stmt->result); stmt->result = NULL; } diff --git a/driver/options.cc b/driver/options.cc index dcc0b547..45c3da1d 100644 --- a/driver/options.cc +++ b/driver/options.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -270,7 +272,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, case SQL_ATTR_AUTOCOMMIT: if (ValuePtr != (SQLPOINTER) SQL_AUTOCOMMIT_ON) { - if (!is_connected(dbc)) + if (dbc->mysql_proxy == nullptr || !dbc->mysql_proxy->is_connected()) { dbc->commit_flag= CHECK_AUTOCOMMIT_OFF; return SQL_SUCCESS; @@ -282,7 +284,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, if (autocommit_on(dbc)) return odbc_stmt(dbc,"SET AUTOCOMMIT=0", SQL_NTS, TRUE); } - else if (!is_connected(dbc)) + else if (dbc->mysql_proxy == nullptr || !dbc->mysql_proxy->is_connected()) { dbc->commit_flag= CHECK_AUTOCOMMIT_ON; return SQL_SUCCESS; @@ -294,7 +296,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, case SQL_ATTR_LOGIN_TIMEOUT: { /* we can't change timeout values in post connect state */ - if (is_connected(dbc)) + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) { return set_conn_error(dbc, MYERR_S1011, NULL, 0); } @@ -338,11 +340,11 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, if (!(db= fix_str((char *)ldb, (char *)ValuePtr, StringLengthPtr))) return set_conn_error((DBC*)hdbc,MYERR_S1009,NULL, 0); - if (is_connected(dbc)) + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) { - if (mysql_select_db(dbc->mysql,(char*) db)) + if (dbc->mysql_proxy->select_db((char*) db)) { - set_conn_error(dbc,MYERR_S1000,mysql_error(dbc->mysql),mysql_errno(dbc->mysql)); + set_conn_error(dbc, MYERR_S1000, dbc->mysql_proxy->error(), dbc->mysql_proxy->error_code()); return SQL_ERROR; } } @@ -365,7 +367,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, case SQL_TRANSLATE_OPTION: { char buff[100]; - sprintf(buff,"Suppose to set this attribute '%d' through driver manager, not by the driver",(int) Attribute); + snprintf(buff, sizeof(buff), "Suppose to set this attribute '%d' through driver manager, not by the driver", (int)Attribute); return set_conn_error((DBC*)hdbc,MYERR_01S02,buff,0); } @@ -373,7 +375,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, break; case SQL_ATTR_TXN_ISOLATION: - if (!is_connected(dbc)) /* no connection yet */ + if (dbc->mysql_proxy == nullptr || !dbc->mysql_proxy->is_connected()) /* no connection yet */ { dbc->txn_isolation= (SQLINTEGER)(SQLLEN)ValuePtr; return SQL_SUCCESS; @@ -395,8 +397,7 @@ MySQLSetConnectAttr(SQLHDBC hdbc, SQLINTEGER Attribute, if (level) { SQLRETURN rc; - sprintf(buff,"SET SESSION TRANSACTION ISOLATION LEVEL %s", - level); + snprintf(buff, sizeof(buff), "SET SESSION TRANSACTION ISOLATION LEVEL %s", level); if (SQL_SUCCEEDED(rc = odbc_stmt(dbc, buff, SQL_NTS, TRUE))) { dbc->txn_isolation= (size_t)ValuePtr; @@ -494,8 +495,8 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, case SQL_ATTR_CONNECTION_DEAD: /* If waking up fails - we return "connection is dead", no matter what really the reason is */ if (dbc->need_to_wakeup != 0 && wakeup_connection(dbc) - || dbc->need_to_wakeup == 0 && mysql_ping(dbc->mysql) && - is_connection_lost(mysql_errno(dbc->mysql))) + || dbc->need_to_wakeup == 0 && dbc->mysql_proxy->ping() && + is_connection_lost(dbc->mysql_proxy->error_code())) *((SQLUINTEGER *)num_attr)= SQL_CD_TRUE; else *((SQLUINTEGER *)num_attr)= SQL_CD_FALSE; @@ -507,14 +508,16 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, break; case SQL_ATTR_CURRENT_CATALOG: - if (is_connected(dbc) && reget_current_catalog(dbc)) - { - return set_handle_error(SQL_HANDLE_DBC, hdbc, MYERR_S1000, - "Unable to get current catalog", 0); - } - else if (is_connected(dbc)) - { - *char_attr= (SQLCHAR *)(!dbc->database.empty() ? dbc->database.c_str() : "null"); + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) { + if (reget_current_catalog(dbc)) + { + return set_handle_error(SQL_HANDLE_DBC, hdbc, MYERR_S1000, + "Unable to get current catalog", 0); + } + else + { + *char_attr = (SQLCHAR*)(!dbc->database.empty() ? dbc->database.c_str() : "null"); + } } else { @@ -536,7 +539,7 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, break; case SQL_ATTR_PACKET_SIZE: - *((SQLUINTEGER *)num_attr)= dbc->mysql->net.max_packet; + *((SQLUINTEGER*)num_attr) = dbc->mysql_proxy->get_max_packet(); break; case SQL_ATTR_TXN_ISOLATION: @@ -550,13 +553,13 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, Unless we're not connected yet, then we just assume it will be REPEATABLE READ, which is the server default. */ - if (!is_connected(dbc)) + if (dbc->mysql_proxy == nullptr || !dbc->mysql_proxy->is_connected()) { *((SQLINTEGER *)num_attr)= SQL_TRANSACTION_REPEATABLE_READ; break; } - if (is_minimum_version(dbc->mysql->server_version, "8.0")) + if (is_minimum_version(dbc->mysql_proxy->get_server_version(), "8.0")) result = odbc_stmt(dbc, "SELECT @@transaction_isolation", SQL_NTS, TRUE); else result = odbc_stmt(dbc, "SELECT @@tx_isolation", SQL_NTS, TRUE); @@ -571,8 +574,8 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, MYSQL_RES *res; MYSQL_ROW row; - if ((res= mysql_store_result(dbc->mysql)) && - (row= mysql_fetch_row(res))) + if ((res= dbc->mysql_proxy->store_result()) && + (row = dbc->mysql_proxy->fetch_row(res))) { if (strncmp(row[0], "READ-UNCOMMITTED", 16) == 0) { dbc->txn_isolation= SQL_TRANSACTION_READ_UNCOMMITTED; @@ -587,7 +590,7 @@ MySQLGetConnectAttr(SQLHDBC hdbc, SQLINTEGER attrib, SQLCHAR **char_attr, dbc->txn_isolation= SQL_TRANSACTION_SERIALIZABLE; } } - mysql_free_result(res); + dbc->mysql_proxy->free_result(res); } } @@ -634,7 +637,7 @@ MySQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, { DESC *desc= (DESC *) ValuePtr; DESC **dest= NULL; - desc_desc_type desc_type; + desc_desc_type desc_type = DESC_UNKNOWN; if (Attribute == SQL_ATTR_APP_PARAM_DESC) { diff --git a/driver/parse.cc b/driver/parse.cc index 0f85d735..bbaf57b2 100644 --- a/driver/parse.cc +++ b/driver/parse.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -374,7 +376,7 @@ const char * find_token(CHARSET_INFO *charset, const char * begin, const char * find_first_token(CHARSET_INFO *charset, const char * begin, const char * end, const char * target) { - const char * token, *before= end; + const char * token; while ((token= mystr_get_next_token(charset, &begin, end)) != end) { @@ -804,35 +806,35 @@ BOOL tokenize(MY_PARSER *parser) /* Returns TRUE if the rule has succeded and type has been identified */ static -BOOL process_rule(MY_PARSER *parser, const QUERY_TYPE_RESOLVING *rule) +BOOL process_rule(MY_PARSER *parser, const QUERY_TYPE_RESOLVING *rule_param) { uint i; char *token; - for (i= rule->pos_from; - i <= myodbc_min(rule->pos_thru > 0 ? rule->pos_thru : rule->pos_from, + for (i= rule_param->pos_from; + i <= myodbc_min(rule_param->pos_thru > 0 ? rule_param->pos_thru : rule_param->pos_from, TOKEN_COUNT(parser->query) - 1); ++i) { token= get_token(parser->query, i); - if (parser->pos && case_compare(parser->query, token, rule->keyword)) + if (parser->pos && case_compare(parser->query, token, rule_param->keyword)) { - if (rule->and_rule) + if (rule_param->and_rule) { - return process_rule(parser, rule->and_rule); + return process_rule(parser, rule_param->and_rule); } else { - parser->query->query_type= rule->query_type; + parser->query->query_type= rule_param->query_type; return TRUE; } } } - if (rule->or_rule) + if (rule_param->or_rule) { - return process_rule(parser, rule->or_rule); + return process_rule(parser, rule_param->or_rule); } return FALSE; @@ -840,16 +842,16 @@ BOOL process_rule(MY_PARSER *parser, const QUERY_TYPE_RESOLVING *rule) QUERY_TYPE_ENUM detect_query_type(MY_PARSER *parser, - const QUERY_TYPE_RESOLVING *rule) + const QUERY_TYPE_RESOLVING *rule_param) { - while (rule->keyword != NULL) + while (rule_param->keyword != NULL) { - if (process_rule(parser, rule)) + if (process_rule(parser, rule_param)) { return parser->query->query_type; } - ++rule; + ++rule_param; } return myqtOther; diff --git a/driver/prepare.cc b/driver/prepare.cc index 61cd1211..fbe04805 100644 --- a/driver/prepare.cc +++ b/driver/prepare.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -344,13 +346,11 @@ SQLRETURN SQL_API SQLParamOptions( SQLHSTMT hstmt, SQLULEN crow, SQLULEN *pirow ) { - SQLINTEGER buflen= SQL_IS_ULEN; #else SQLRETURN SQL_API SQLParamOptions( SQLHSTMT hstmt, SQLUINTEGER crow, SQLUINTEGER *pirow ) { - SQLINTEGER buflen= SQL_IS_UINTEGER; #endif SQLRETURN rc; STMT *stmt= (STMT *)hstmt; diff --git a/driver/query_parsing.cc b/driver/query_parsing.cc new file mode 100644 index 00000000..dd24f25e --- /dev/null +++ b/driver/query_parsing.cc @@ -0,0 +1,155 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "driver/query_parsing.h" + +// Helper function to advance the index of query_str to after the closing quotation. +// The passed in index is assumed to be the position of the opening quotation mark. +// Any escaped quotation marks will be ignored. +void scan_quotation(const std::string& query_str, size_t& index, char quotation_mark) +{ + bool escaped = false; + while (++index < query_str.size()) + { + if (query_str[index] == '\\') + { + escaped = !escaped; + } + else + { + // The quotation mark must not be escaped for it to be a valid + // closing quotation mark. + if (!escaped && query_str[index] == quotation_mark) + { + index++; + return; + } + + escaped = false; + } + } +} + +// Helper function to determine if the character at position index in query_str +// (which is assumed to be ' ', '/r', '/n', or '/t' and outside of any quotations) +// can be safely removed from the statement without changing the meaning of the statement. +bool is_character_unnecessary(const std::string& query_str, const size_t& index) +{ + // The character is unnecessary if it's the first or last character. + if (index == 0 || index == query_str.size() - 1) + { + return true; + } + + // The character is unnecessary if the next character is also similar + // to space or is a statement separator (ie. ;). + switch (query_str[index + 1]) + { + case ' ': + case '\r': + case '\n': + case '\t': + case ';': + return true; + default: + return false; + } +} + +/** + Splits up user provided query text into a vector of individual + statements (separated by ; in the original query). + Each statement is cleaned of any unnecessary characters. + + @param[in] original_query The query being processed + */ +std::vector parse_query_into_statements(const char* original_query) +{ + std::vector statements; + std::string query_str(original_query); + + size_t index = 0; + while (index < query_str.size()) + { + switch (query_str[index]) + { + // If we find a quotation then advance the index + // to after the closing quotation. + case '\"': + case '\'': + scan_quotation(query_str, index, query_str[index]); + break; + + // If we find a space-like character then either remove it + // or replace it with a space. + case ' ': + case '\r': + case '\n': + case '\t': + if (is_character_unnecessary(query_str, index)) + { + query_str.erase(index, 1); + } + else + { + if (query_str[index] != ' ') + { + query_str.replace(index, 1, " "); + } + index++; + } + break; + + // If we find the statement separator ; then we are ready to + // build our statement string. + case ';': + if (index > 0) + { + std::string statement = query_str.substr(0, index); + statements.push_back(statement); + } + + query_str = query_str.substr(index + 1, query_str.size() - 1); + index = 0; + break; + + default: + index++; + } + } + + // If we still have any query_str left then it must be + // the final statement with no separator. + if (index > 0 && index == query_str.size()) + { + statements.push_back(query_str); + } + + return statements; +} diff --git a/driver/query_parsing.h b/driver/query_parsing.h new file mode 100644 index 00000000..160af95a --- /dev/null +++ b/driver/query_parsing.h @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __QUERY_PARSING_H__ +#define __QUERY_PARSING_H__ + +#include +#include + +std::vector parse_query_into_statements(const char* original_query); + +#endif /* __QUERY_PARSING_H__ */ diff --git a/driver/results.cc b/driver/results.cc old mode 100755 new mode 100644 index 8dc3bfa0..5e61210d --- a/driver/results.cc +++ b/driver/results.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -380,8 +382,8 @@ sql_get_data(STMT *stmt, SQLSMALLINT fCType, uint column_number, SQLPOINTER rgbValue, SQLLEN cbValueMax, SQLLEN *pcbValue, char *value, ulong length, DESCREC *arrec) { - MYSQL_FIELD *field= mysql_fetch_field_direct(stmt->result, column_number); - SQLLEN tmp; + MYSQL_FIELD *field= stmt->dbc->mysql_proxy->fetch_field_direct(stmt->result, column_number); + SQLLEN temp; long long numeric_value = 0; unsigned long long u_numeric_value = 0; my_bool convert= 1; @@ -449,7 +451,7 @@ sql_get_data(STMT *stmt, SQLSMALLINT fCType, uint column_number, if (!pcbValue) { - pcbValue= &tmp; /* Easier code */ + pcbValue= &temp; /* Easier code */ } if (field->type == MYSQL_TYPE_BIT) @@ -909,9 +911,9 @@ sql_get_data(STMT *stmt, SQLSMALLINT fCType, uint column_number, just reverse binary data */ char _value[21]; /* max string length of 64bit number */ if (numeric_value) - sprintf(_value, "%ll", numeric_value); + snprintf(_value, sizeof(_value), "%ll", numeric_value); else - sprintf(_value, "%llu", u_numeric_value); + snprintf(_value, sizeof(_value), "%llu", u_numeric_value); sqlnum_from_str(_value, sqlnum, &overflow); @@ -1594,10 +1596,10 @@ SQLRETURN SQL_API SQLMoreResults( SQLHSTMT hstmt ) /* try to get next resultset */ nRetVal = next_result(stmt); - /* call to mysql_next_result() failed */ + /* call to next_result() failed */ if (nRetVal > 0) { - nRetVal= mysql_errno(stmt->dbc->mysql); + nRetVal= stmt->dbc->mysql_proxy->error_code(); switch ( nRetVal ) { @@ -1606,14 +1608,18 @@ SQLRETURN SQL_API SQLMoreResults( SQLHSTMT hstmt ) #if MYSQL_VERSION_ID > 80023 case ER_CLIENT_INTERACTION_TIMEOUT: #endif - nReturn = stmt->set_error("08S01", mysql_error( stmt->dbc->mysql ), nRetVal ); + const char *error_code, *error_msg; + if (stmt->dbc->fh->trigger_failover_if_needed("08S01", error_code, error_msg)) + nReturn = stmt->set_error(error_code, error_msg, 0); + else + nReturn = stmt->set_error(error_code, stmt->dbc->mysql_proxy->error(), nRetVal); goto exitSQLMoreResults; case CR_COMMANDS_OUT_OF_SYNC: case CR_UNKNOWN_ERROR: nReturn = stmt->set_error("HY000"); goto exitSQLMoreResults; default: - nReturn = stmt->set_error("HY000", "unhandled error from mysql_next_result()", nRetVal ); + nReturn = stmt->set_error("HY000", "unhandled error from next_result()", nRetVal ); goto exitSQLMoreResults; } } @@ -1768,7 +1774,6 @@ fill_fetch_bookmark_buffers(STMT *stmt, ulong value, uint rownum) { SQLLEN *pcbValue= NULL; SQLPOINTER TargetValuePtr= NULL; - ulong copy_bytes= 0; stmt->reset_getdata_position(); @@ -2027,7 +2032,7 @@ SQLRETURN SQL_API myodbc_single_fetch( SQLHSTMT hstmt, stmt->set_error("01S07", "One or more row has error.", 0); return SQL_SUCCESS_WITH_INFO; //SQL_NO_DATA_FOUND case SQL_ERROR: return stmt->set_error(MYERR_S1000, - mysql_error(stmt->dbc->mysql), 0); + stmt->dbc->mysql_proxy->error(), 0); } } else @@ -2173,7 +2178,7 @@ SQLRETURN SQL_API myodbc_single_fetch( SQLHSTMT hstmt, stmt->rows_found_in_set= 1; *pcrow= cur_row; - disconnected= is_connection_lost(mysql_errno(stmt->dbc->mysql)) + disconnected= is_connection_lost(stmt->dbc->mysql_proxy->error_code()) && handle_connection_error(stmt); if ( upd_status && stmt->ird->rows_processed_ptr ) @@ -2250,7 +2255,6 @@ SQLRETURN SQL_API my_SQLExtendedFetch( SQLHSTMT hstmt, MYSQL_ROW_OFFSET save_position= 0; SQLULEN dummy_pcrow; BOOL disconnected= FALSE; - long brow= 0; DECLARE_LOCALE_HANDLE try { @@ -2266,7 +2270,7 @@ SQLRETURN SQL_API my_SQLExtendedFetch( SQLHSTMT hstmt, return SQL_NO_DATA_FOUND; case OPS_STREAMS_PENDING: /* Magical out params fetch */ - mysql_stmt_fetch(stmt->ssps); + stmt->dbc->mysql_proxy->stmt_fetch(stmt->ssps); default: /* TODO: Need to remember real fetch' result */ /* just in case... */ @@ -2426,7 +2430,7 @@ SQLRETURN SQL_API my_SQLExtendedFetch( SQLHSTMT hstmt, if (res != row_res || res != row_book) { /* Any successful row makes overall result SQL_SUCCESS_WITH_INFO */ - if (SQL_SUCCEEDED(row_res) && SQL_SUCCEEDED(row_res)) + if (SQL_SUCCEEDED(row_res)) { res= SQL_SUCCESS_WITH_INFO; } @@ -2463,7 +2467,7 @@ SQLRETURN SQL_API my_SQLExtendedFetch( SQLHSTMT hstmt, stmt->rows_found_in_set= i; *pcrow= i; - disconnected= is_connection_lost(mysql_errno(stmt->dbc->mysql)) + disconnected= is_connection_lost(stmt->dbc->mysql_proxy->error_code()) && handle_connection_error(stmt); if ( upd_status && stmt->ird->rows_processed_ptr ) diff --git a/driver/topology_service.cc b/driver/topology_service.cc new file mode 100644 index 00000000..2d81f0ea --- /dev/null +++ b/driver/topology_service.cc @@ -0,0 +1,336 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "cluster_aware_metrics_container.h" +#include "topology_service.h" + +TOPOLOGY_SERVICE::TOPOLOGY_SERVICE(unsigned long dbc_id, bool enable_logging) + : dbc_id{dbc_id}, + cluster_instance_host{nullptr}, + refresh_rate_in_ms{DEFAULT_REFRESH_RATE_IN_MILLISECONDS}, + metrics_container{std::make_shared()}{ + + // TODO get better initial cluster id + time_t now = time(0); + cluster_id = std::to_string(now) + ctime(&now); + if (enable_logging) + logger = init_log_file(); +} + +TOPOLOGY_SERVICE::TOPOLOGY_SERVICE(const TOPOLOGY_SERVICE& ts) { + refresh_rate_in_ms = ts.refresh_rate_in_ms; + cluster_id = ts.cluster_id; + cluster_instance_host = ts.cluster_instance_host; + logger = ts.logger; + dbc_id = ts.dbc_id; + metrics_container = ts.metrics_container; +} + +TOPOLOGY_SERVICE::~TOPOLOGY_SERVICE() { + if (cluster_instance_host) + cluster_instance_host.reset(); +} + +void TOPOLOGY_SERVICE::set_cluster_id(std::string cid) { + MYLOG_TRACE(logger.get(), dbc_id, "[TOPOLOGY_SERVICE] cluster ID=%s", cid.c_str()); + this->cluster_id = cid; + metrics_container->set_cluster_id(this->cluster_id); +} + +void TOPOLOGY_SERVICE::set_cluster_instance_template(std::shared_ptr host_template) { + + // NOTE, this may not have to be a pointer. Copy the information passed to this function. + // Alernatively the create host function should be part of the Topologyservice, even protected or private one + // and host information passed as separate parameters. + if (cluster_instance_host) + cluster_instance_host.reset(); + + MYLOG_TRACE(logger.get(), dbc_id, + "[TOPOLOGY_SERVICE] cluster instance host=%s, port=%d", + host_template->get_host().c_str(), host_template->get_port()); + cluster_instance_host = host_template; +} + +void TOPOLOGY_SERVICE::set_refresh_rate(int refresh_rate) { + refresh_rate_in_ms = refresh_rate; +} + +std::shared_ptr TOPOLOGY_SERVICE::get_last_used_reader() { + auto topology_info = get_from_cache(); + if (!topology_info || refresh_needed(topology_info->time_last_updated())) { + return nullptr; + } + + return topology_info->get_last_used_reader(); +} + +void TOPOLOGY_SERVICE::set_last_used_reader(std::shared_ptr reader) { + if (reader) { + std::unique_lock lock(topology_cache_mutex); + auto topology_info = get_from_cache(); + if (topology_info) { + topology_info->set_last_used_reader(reader); + } + lock.unlock(); + } +} + +std::set TOPOLOGY_SERVICE::get_down_hosts() { + std::set down_hosts; + + std::unique_lock lock(topology_cache_mutex); + auto topology_info = get_from_cache(); + if (topology_info) { + down_hosts = topology_info->get_down_hosts(); + } + lock.unlock(); + + return down_hosts; +} + +void TOPOLOGY_SERVICE::mark_host_down(std::shared_ptr host) { + if (!host) { + return; + } + + std::unique_lock lock(topology_cache_mutex); + + auto topology_info = get_from_cache(); + if (topology_info) { + topology_info->mark_host_down(host); + } + + lock.unlock(); +} + +void TOPOLOGY_SERVICE::mark_host_up(std::shared_ptr host) { + if (!host) { + return; + } + + std::unique_lock lock(topology_cache_mutex); + + auto topology_info = get_from_cache(); + if (topology_info) { + topology_info->mark_host_up(host); + } + + lock.unlock(); +} + +void TOPOLOGY_SERVICE::set_gather_metric(bool can_gather) { + this->metrics_container->set_gather_metric(can_gather); +} + +void TOPOLOGY_SERVICE::clear_all() { + std::unique_lock lock(topology_cache_mutex); + topology_cache.clear(); + lock.unlock(); +} + +void TOPOLOGY_SERVICE::clear() { + std::unique_lock lock(topology_cache_mutex); + topology_cache.erase(cluster_id); + lock.unlock(); +} + +std::shared_ptr TOPOLOGY_SERVICE::get_cached_topology() { + return get_from_cache(); +} + +//TODO consider the return value +//Note to determine whether or not force_update succeeded one would compare +// CLUSTER_TOPOLOGY_INFO->time_last_updated() prior and after the call if non-null information was given prior. +std::shared_ptr TOPOLOGY_SERVICE::get_topology(MYSQL_PROXY* connection, bool force_update) +{ + //TODO reconsider using this cache. It appears that we only store information for the current cluster Id. + // therefore instead of a map we can just keep CLUSTER_TOPOLOGY_INFO* topology_info member variable. + auto cached_topology = get_from_cache(); + if (!cached_topology + || force_update + || refresh_needed(cached_topology->time_last_updated())) + { + auto latest_topology = query_for_topology(connection); + if (latest_topology) { + put_to_cache(latest_topology); + return latest_topology; + } + } + + return cached_topology; +} + +// TODO consider thread safety and usage of pointers +std::shared_ptr TOPOLOGY_SERVICE::get_from_cache() { + if (topology_cache.empty()) { + metrics_container->register_use_cached_topology(false); + return nullptr; + } + + auto result = topology_cache.find(cluster_id); + if (result == topology_cache.end()) { + metrics_container->register_use_cached_topology(false); + return nullptr; + } + + metrics_container->register_use_cached_topology(true); + return result->second; +} + +// TODO consider thread safety and usage of pointers +void TOPOLOGY_SERVICE::put_to_cache(std::shared_ptr topology_info) { + if (!topology_cache.empty()) + { + auto result = topology_cache.find(cluster_id); + if (result != topology_cache.end()) { + result->second.reset(); + result->second = topology_info; + return; + } + } + std::unique_lock lock(topology_cache_mutex); + topology_cache[cluster_id] = topology_info; + lock.unlock(); +} + +MYSQL_RES* TOPOLOGY_SERVICE::try_execute_query(MYSQL_PROXY* mysql_proxy, const char* query) { + if (mysql_proxy != nullptr && mysql_proxy->query(query) == 0) { + return mysql_proxy->store_result(); + } + + return nullptr; +} + +// TODO harmonize time function across objects so the times are comparable +bool TOPOLOGY_SERVICE::refresh_needed(std::time_t last_updated) { + + return time(0) - last_updated > (refresh_rate_in_ms / 1000); +} + +std::shared_ptr TOPOLOGY_SERVICE::create_host(MYSQL_ROW& row) { + + //TEMP and TODO figure out how to fetch values from row by name, not by ordinal for now this enum is matching + // order of columns in the query + enum COLUMNS { + SERVER_ID, + SESSION, + LAST_UPDATE_TIMESTAMP, + REPLICA_LAG_MILLISECONDS + }; + + if (row[SERVER_ID] == NULL) { + return nullptr; // will not be able to generate host endpoint so no point. TODO: log this condition? + } + + std::string host_endpoint = get_host_endpoint(row[SERVER_ID]); + + //TODO check cluster_instance_host for NULL, or decide what is needed out of it + std::shared_ptr host_info = std::make_shared( + host_endpoint, cluster_instance_host->get_port()); + + //TODO do case-insensitive comparison + // side note: how stable this is on the server side? If it changes there we will not detect a writer. + if (strcmp(row[SESSION], WRITER_SESSION_ID) == 0) + { + host_info->mark_as_writer(true); + } + + host_info->instance_name = row[SERVER_ID] ? row[SERVER_ID] : ""; + host_info->session_id = row[SESSION] ? row[SESSION] : ""; + host_info->last_updated = row[LAST_UPDATE_TIMESTAMP] ? row[LAST_UPDATE_TIMESTAMP] : ""; + host_info->replica_lag = row[REPLICA_LAG_MILLISECONDS] ? row[REPLICA_LAG_MILLISECONDS] : ""; + + return host_info; +} + +// If no host information retrieved return NULL +std::shared_ptr TOPOLOGY_SERVICE::query_for_topology(MYSQL_PROXY* mysql_proxy) { + + std::shared_ptr topology_info = nullptr; + + std::chrono::steady_clock::time_point start_time_ms = std::chrono::steady_clock::now(); + if (MYSQL_RES* result = try_execute_query(mysql_proxy, RETRIEVE_TOPOLOGY_SQL)) { + topology_info = std::make_shared(); + std::map> instances; + MYSQL_ROW row; + int writer_count = 0; + while ((row = mysql_proxy->fetch_row(result))) { + std::shared_ptr host_info = create_host(row); + if (host_info) { + // Only mark the first/latest writer as true writer + if (host_info->is_host_writer()) { + if (writer_count > 0) { + host_info->mark_as_writer(false); + } + writer_count++; + } + // Add to topology if instance not seen before + if (!TOPOLOGY_SERVICE::does_instance_exist(instances, host_info)) { + topology_info->add_host(host_info); + } + } + } + mysql_proxy->free_result(result); + + topology_info->is_multi_writer_cluster = writer_count > 1; + if (writer_count == 0) { + MYLOG_TRACE(logger.get(), dbc_id, + "[TOPOLOGY_SERVICE] The topology query returned an " + "invalid topology - no writer instance detected"); + } + } + + std::chrono::steady_clock::time_point end_time_ms = std::chrono::steady_clock::now(); + long long elasped_time_ms = std::chrono::duration_cast(end_time_ms - start_time_ms).count(); + metrics_container->register_topology_query_execution_time(elasped_time_ms); + return topology_info; +} + +bool TOPOLOGY_SERVICE::does_instance_exist( + std::map>& instances, + std::shared_ptr host_info) { + + auto duplicate = instances.find(host_info->instance_name); + if (duplicate != instances.end()) { + return true; + } else { + instances.insert(std::pair>( + host_info->instance_name, host_info)); + return false; + } +} + +std::string TOPOLOGY_SERVICE::get_host_endpoint(const char* node_name) { + std::string host = cluster_instance_host->get_host(); + size_t position = host.find("?"); + if (position != std::string::npos) { + host.replace(position, 1, node_name); + } + return host; +} diff --git a/driver/topology_service.h b/driver/topology_service.h new file mode 100644 index 00000000..a95ed8b5 --- /dev/null +++ b/driver/topology_service.h @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#ifndef __TOPOLOGYSERVICE_H__ +#define __TOPOLOGYSERVICE_H__ + +#include "cluster_aware_metrics_container.h" +#include "cluster_topology_info.h" +#include "mysql_proxy.h" +#include "mylog.h" + +#include +#include +#include +#include +#include + +// TODO - consider - do we really need miliseconds for refresh? - the default numbers here are already 30 seconds.000; +#define DEFAULT_REFRESH_RATE_IN_MILLISECONDS 30000 +#define WRITER_SESSION_ID "MASTER_SESSION_ID" +#define RETRIEVE_TOPOLOGY_SQL "SELECT SERVER_ID, SESSION_ID, LAST_UPDATE_TIMESTAMP, REPLICA_LAG_IN_MILLISECONDS \ + FROM information_schema.replica_host_status \ + WHERE time_to_sec(timediff(now(), LAST_UPDATE_TIMESTAMP)) <= 300 \ + ORDER BY LAST_UPDATE_TIMESTAMP DESC" + +static std::map> topology_cache; +static std::mutex topology_cache_mutex; + +class TOPOLOGY_SERVICE { +public: + TOPOLOGY_SERVICE(unsigned long dbc_id, bool enable_logging = false); + TOPOLOGY_SERVICE(const TOPOLOGY_SERVICE&); + virtual ~TOPOLOGY_SERVICE(); + + virtual void set_cluster_id(std::string cluster_id); + virtual void set_cluster_instance_template(std::shared_ptr host_template); //is this equivalent to setcluster_instance_host + + virtual std::shared_ptr get_topology( + MYSQL_PROXY* connection, bool force_update = false); + std::shared_ptr get_cached_topology(); + + std::shared_ptr get_last_used_reader(); + void set_last_used_reader(std::shared_ptr reader); + std::set get_down_hosts(); + virtual void mark_host_down(std::shared_ptr host); + virtual void mark_host_up(std::shared_ptr host); + void set_refresh_rate(int refresh_rate); + void set_gather_metric(bool can_gather); + void clear_all(); + void clear(); + + // Property Keys + const std::string SESSION_ID = "TOPOLOGY_SERVICE_SESSION_ID"; + const std::string LAST_UPDATED = "TOPOLOGY_SERVICE_LAST_UPDATE_TIMESTAMP"; + const std::string REPLICA_LAG = "TOPOLOGY_SERVICE_REPLICA_LAG_IN_MILLISECONDS"; + const std::string INSTANCE_NAME = "TOPOLOGY_SERVICE_SERVER_ID"; + +private: + const int DEFAULT_CACHE_EXPIRE_MS = 5 * 60 * 1000; // 5 min + + const std::string GET_INSTANCE_NAME_SQL = "SELECT @@aurora_server_id"; + const std::string GET_INSTANCE_NAME_COL = "@@aurora_server_id"; + + const std::string FIELD_SERVER_ID = "SERVER_ID"; + const std::string FIELD_SESSION_ID = "SESSION_ID"; + const std::string FIELD_LAST_UPDATED = "LAST_UPDATE_TIMESTAMP"; + const std::string FIELD_REPLICA_LAG = "REPLICA_LAG_IN_MILLISECONDS"; + + std::shared_ptr logger = nullptr; + unsigned long dbc_id = 0; + +protected: + const int NO_CONNECTION_INDEX = -1; + int refresh_rate_in_ms; + + std::string cluster_id; + std::shared_ptr cluster_instance_host; + + std::shared_ptr metrics_container; + + bool refresh_needed(std::time_t last_updated); + std::shared_ptr query_for_topology(MYSQL_PROXY* connection); + std::shared_ptr create_host(MYSQL_ROW& row); + std::string get_host_endpoint(const char* node_name); + static bool does_instance_exist( + std::map>& instances, + std::shared_ptr host_info); + + std::shared_ptr get_from_cache(); + void put_to_cache(std::shared_ptr topology_info); + + MYSQL_RES* try_execute_query(MYSQL_PROXY* mysql_proxy, const char* query); +}; + +#endif /* __TOPOLOGYSERVICE_H__ */ diff --git a/driver/transact.cc b/driver/transact.cc index 906ecfe8..86debcdc 100644 --- a/driver/transact.cc +++ b/driver/transact.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -59,7 +61,7 @@ static SQLRETURN my_transact(SQLHDBC hdbc, SQLSMALLINT CompletionType) case SQL_ROLLBACK: if (!trans_supported(dbc)) { - return set_conn_error((DBC*)hdbc,MYERR_S1C00, + return set_conn_error(dbc, MYERR_S1C00, "Underlying server does not support transactions, upgrade to version >= 3.23.38",0); } query= "ROLLBACK"; @@ -67,18 +69,16 @@ static SQLRETURN my_transact(SQLHDBC hdbc, SQLSMALLINT CompletionType) break; default: - return set_conn_error((DBC*)hdbc,MYERR_S1012,NULL,0); + return set_conn_error(dbc, MYERR_S1012, NULL, 0); } - MYLOG_DBC_QUERY(dbc, query); + MYLOG_DBC_TRACE(dbc, query); - LOCK_DBC(dbc); - if (check_if_server_is_alive(dbc) || - mysql_real_query(dbc->mysql,query,length)) + result = odbc_stmt(dbc, query, length, true); + + if (SQL_SUCCEEDED(result)) { - result= set_conn_error((DBC*)hdbc,MYERR_S1000, - mysql_error(dbc->mysql), - mysql_errno(dbc->mysql)); + dbc->transaction_open = false; } } return(result); diff --git a/driver/unicode.cc b/driver/unicode.cc index 60cccda7..203388db 100644 --- a/driver/unicode.cc +++ b/driver/unicode.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -285,7 +287,7 @@ SQLDescribeColW(SQLHSTMT hstmt, SQLUSMALLINT column, if (free_value == -1) { - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } @@ -297,7 +299,7 @@ SQLDescribeColW(SQLHSTMT hstmt, SQLUSMALLINT column, { if (free_value) x_free(value); - set_mem_error(stmt->dbc->mysql); + set_mem_error(stmt->dbc->mysql_proxy); return handle_connection_error(stmt); } @@ -1028,7 +1030,7 @@ SQLSetConnectAttrWImpl(SQLHDBC hdbc, SQLINTEGER attribute, "than 0 but was not SQL_NTS " , 0); } - if (is_connected(dbc)) + if (dbc->mysql_proxy != nullptr && dbc->mysql_proxy->is_connected()) value= sqlwchar_as_sqlchar(dbc->cxn_charset_info, (SQLWCHAR*)value, &len, &errors); else diff --git a/driver/utility.cc b/driver/utility.cc index 27f8ae38..2fe192cf 100755 --- a/driver/utility.cc +++ b/driver/utility.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -81,21 +83,19 @@ SQLRETURN exec_stmt_query_std(STMT *stmt, const std::string &query, return odbc_stmt(stmt->dbc, query.c_str(), query.size(), req_lock); } -/** - - /** Execute a SQL statement. - @param[in] dbc The database connection - @param[in] query The query to execute - @param[in] req_lock The flag if dbc->lock thread lock should be used - when executing a query + @param[in] dbc The database connection + @param[in] query The query to execute + @param[in] query_length The string length of the query + @param[in] req_lock The flag if dbc->lock thread lock should be used + when executing a query */ SQLRETURN odbc_stmt(DBC *dbc, const char *query, SQLULEN query_length, my_bool req_lock) { - SQLRETURN result= SQL_SUCCESS; + SQLRETURN result = SQL_SUCCESS; LOCK_DBC_DEFER(dbc); if (req_lock) @@ -105,20 +105,55 @@ SQLRETURN odbc_stmt(DBC *dbc, const char *query, if (query_length == SQL_NTS) { - query_length= strlen(query); + query_length = strlen(query); + } + + // return immediately if not connected + if (!dbc->mysql_proxy->is_connected()) + { + return set_conn_error(dbc, MYERR_08S01, "The active SQL connection was lost. Please discard this connection.", 0); } - if ( check_if_server_is_alive(dbc) || - mysql_real_query(dbc->mysql, query, query_length) ) + bool server_alive = is_server_alive(dbc); + if (!server_alive || dbc->mysql_proxy->real_query(query, query_length)) { - result= set_conn_error(dbc,MYERR_S1000,mysql_error(dbc->mysql), - mysql_errno(dbc->mysql)); + const unsigned int mysql_error_code = dbc->mysql_proxy->error_code(); + + MYLOG_DBC_TRACE(dbc, dbc->mysql_proxy->error()); + result = set_conn_error(dbc, MYERR_S1000, dbc->mysql_proxy->error(), mysql_error_code); + + if (!server_alive || is_connection_lost(mysql_error_code)) + { + bool rollback = (!autocommit_on(dbc) && trans_supported(dbc)) || dbc->transaction_open; + if (rollback) + { + MYLOG_DBC_TRACE(dbc, "Rolling back"); + dbc->mysql_proxy->real_query("ROLLBACK", 8); + } + + const char *error_code, *error_msg; + if (dbc->fh->trigger_failover_if_needed("08S01", error_code, error_msg)) + { + if (strcmp(error_code, "08007") == 0) + { + result = set_conn_error(dbc, MYERR_08007, "Connection failure during transaction.", 0); + } + else if (strcmp(error_code, "08S02") == 0) + { + result = set_conn_error(dbc, MYERR_08S02, "The active SQL connection has changed.", 0); + } + else { + result = set_conn_error(dbc, MYERR_08S01, "The active SQL connection was lost.", 0); + } + } + + dbc->transaction_open = false; + } } return result; } - /** Link a list of fields to the current statement result. @@ -162,7 +197,7 @@ void fix_row_lengths(STMT *stmt, const long* fix_rules, uint row, uint field_cou return; row_lengths = stmt->lengths.get() + row * field_count; - orig_lengths= mysql_fetch_lengths(stmt->result); + orig_lengths = stmt->dbc->mysql_proxy->fetch_lengths(stmt->result); for (i= 0; i < field_count; ++i) { @@ -1448,11 +1483,11 @@ void sqlulen_to_str(char *buff, SQLULEN value) @return void */ -SQLLEN fill_display_size_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) +SQLLEN fill_display_size_buff(char *buff, size_t buff_size, STMT *stmt, MYSQL_FIELD *field) { /* See comment for fill_transfer_oct_len_buff()*/ SQLLEN size= get_display_size(stmt, field); - sprintf(buff,size == SQL_NO_TOTAL ? "%d" : (sizeof(SQLLEN) == 4 ? "%lu" : "%lld"), size); + snprintf(buff, buff_size, size == SQL_NO_TOTAL ? "%d" : (sizeof(SQLLEN) == 4 ? "%lu" : "%lld"), size); return size; } @@ -1466,7 +1501,7 @@ SQLLEN fill_display_size_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) @return void */ -SQLLEN fill_transfer_oct_len_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) +SQLLEN fill_transfer_oct_len_buff(char *buff, size_t buff_size, STMT *stmt, MYSQL_FIELD *field) { /* The only possible negative value get_transfer_octet_length can return is SQL_NO_TOTAL But it can return value which is greater that biggest signed integer(%ld). @@ -1475,7 +1510,7 @@ SQLLEN fill_transfer_oct_len_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) */ SQLLEN len= get_transfer_octet_length(stmt, field); - sprintf(buff, len == SQL_NO_TOTAL ? "%d" : (sizeof(SQLLEN) == 4 ? "%lu" : "%lld"), len ); + snprintf(buff, buff_size, len == SQL_NO_TOTAL ? "%d" : (sizeof(SQLLEN) == 4 ? "%lu" : "%lld"), len); return len; } @@ -1489,11 +1524,11 @@ SQLLEN fill_transfer_oct_len_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) @return void */ -SQLULEN fill_column_size_buff(char *buff, STMT *stmt, MYSQL_FIELD *field) +SQLULEN fill_column_size_buff(char *buff, size_t buff_size, STMT *stmt, MYSQL_FIELD *field) { SQLULEN size= get_column_size(stmt, field); - sprintf(buff, (size== SQL_NO_TOTAL ? "%d" : - (sizeof(SQLULEN) == 4 ? "%lu" : "%llu")), size); + snprintf(buff, buff_size, (size== SQL_NO_TOTAL ? "%d" : + (sizeof(SQLULEN) == 4 ? "%lu" : "%llu")), size); return size; } @@ -2207,7 +2242,8 @@ int str_to_ts(SQL_TIMESTAMP_STRUCT *ts, const char *str, int len, int zeroToMin, BOOL dont_use_set_locale) { uint year, length; - char buff[DATETIME_DIGITS + 1], *to; + char buff[DATETIME_DIGITS + 1] = {0}; + char* to; const char *end; SQL_TIMESTAMP_STRUCT tmp_timestamp; SQLUINTEGER fraction; @@ -2376,7 +2412,8 @@ my_bool str_to_time_st(SQL_TIME_STRUCT *ts, const char *str) my_bool str_to_date(SQL_DATE_STRUCT *rgbValue, const char *str, uint length, int zeroToMin) { - uint field_length,year_length,digits,i,date[3]; + uint field_length,year_length,digits,i; + uint date[3] = {0}; const char *pos; const char *end= str+length; for ( ; !isdigit(*str) && str != end ; ++str ) ; @@ -2477,14 +2514,14 @@ ulong str_to_time_as_long(const char *str, uint length) the server is up with mysql_ping (to force a reconnect) */ -int check_if_server_is_alive( DBC *dbc ) +bool is_server_alive( DBC *dbc ) { time_t seconds= (time_t) time( (time_t*)0 ); - int result= 0; + bool server_alive = true; if ( (ulong)(seconds - dbc->last_query_time) >= CHECK_IF_ALIVE ) { - if ( mysql_ping( dbc->mysql ) ) + if ( dbc->mysql_proxy->ping() ) { /* BUG: 14639 @@ -2501,13 +2538,13 @@ int check_if_server_is_alive( DBC *dbc ) PAH - 9.MAR.06 */ - if (is_connection_lost(mysql_errno( dbc->mysql ))) - result = 1; + if (is_connection_lost(dbc->mysql_proxy->error_code())) + server_alive = false; } } dbc->last_query_time = seconds; - return result; + return server_alive; } @@ -2543,8 +2580,8 @@ int reget_current_catalog(DBC *dbc) MYSQL_RES *res; MYSQL_ROW row; - if ( (res= mysql_store_result(dbc->mysql)) && - (row= mysql_fetch_row(res)) ) + if ( (res= dbc->mysql_proxy->store_result()) && + (row = dbc->mysql_proxy->fetch_row(res))) { /* if (cmp_database(row[0], dbc->database)) */ { @@ -2554,7 +2591,7 @@ int reget_current_catalog(DBC *dbc) } } } - mysql_free_result(res); + dbc->mysql_proxy->free_result(res); } return 0; @@ -2621,84 +2658,6 @@ void free_internal_result_buffers(STMT *stmt) stmt->alloc_root.Clear(); } -/* - @type : myodbc3 internal - @purpose : logs the queries sent to server -*/ - -void query_print(FILE *log_file,char *query) -{ - if ( log_file && query ) - { - /* - because of bug 68201 we bring the result of time() call - to 64-bits in any case - */ - long long time_now= time(NULL); - - fprintf(log_file, "%lld:%s;\n", time_now, query); - } -} - - -FILE *init_query_log(void) -{ - FILE *query_log; -#ifdef _WIN32 - char filename[MAX_PATH]; - size_t buffsize; - - getenv_s(&buffsize, filename, sizeof(filename), "TEMP"); - - if (buffsize) - { - sprintf(filename + buffsize - 1, "\\%s", DRIVER_QUERY_LOGFILE); - } - else - { - sprintf(filename, "c:\\%s", DRIVER_QUERY_LOGFILE); - } - - if ( (query_log= fopen(filename, "a+")) ) -#else - if ( (query_log= fopen(DRIVER_QUERY_LOGFILE, "a+")) ) -#endif - { - fprintf(query_log,"-- Query logging\n"); - fprintf(query_log,"--\n"); - fprintf(query_log,"-- Driver name: %s Version: %s\n",DRIVER_NAME, - DRIVER_VERSION); -#ifdef HAVE_LOCALTIME_R - { - time_t now= time(NULL); - struct tm start; - localtime_r(&now,&start); - - fprintf(query_log,"-- Timestamp: %02d%02d%02d %2d:%02d:%02d\n", - start.tm_year % 100, - start.tm_mon+1, - start.tm_mday, - start.tm_hour, - start.tm_min, - start.tm_sec); - } -#endif /* HAVE_LOCALTIME_R */ - fprintf(query_log,"\n"); - } - return query_log; -} - - -void end_query_log(FILE *query_log) -{ - if ( query_log ) - { - fclose(query_log); - query_log= 0; - } -} - - my_bool is_minimum_version(const char *server_version,const char *version) { /* @@ -3199,7 +3158,6 @@ void sqlnum_to_str(SQL_NUMERIC_STRUCT *sqlnum, SQLCHAR *numstr, /* add zeros for negative scale */ if (reqscale < 0) { - int i; reqscale *= -1; for (i= 1; i <= calcprec; ++i) *(numstr + i - reqscale)= *(numstr + i); @@ -3277,10 +3235,10 @@ SQLRETURN set_sql_select_limit(DBC *dbc, SQLULEN lim_value, my_bool req_lock) return SQL_SUCCESS; if (lim_value > 0 && lim_value < sql_select_unlimited) - sprintf(query, "set @@sql_select_limit=%lu", (unsigned long)lim_value); + snprintf(query, sizeof(query), "set @@sql_select_limit=%lu", (unsigned long)lim_value); else { - strcpy(query, "set @@sql_select_limit=DEFAULT"); + strncpy(query, "set @@sql_select_limit=DEFAULT", sizeof(query)); lim_value= 0; } @@ -3704,7 +3662,8 @@ Gets parameter columns size Returns parameter octet length */ SQLLEN proc_get_param_col_len(STMT *stmt, int sql_type_index, SQLULEN col_size, - SQLSMALLINT decimal_digits, unsigned int flags, char * str_buff) + SQLSMALLINT decimal_digits, unsigned int flags, + char * str_buff, size_t buff_size) { MYSQL_FIELD temp_fld; @@ -3720,7 +3679,7 @@ SQLLEN proc_get_param_col_len(STMT *stmt, int sql_type_index, SQLULEN col_size, if (str_buff != NULL) { - return fill_column_size_buff(str_buff, stmt, &temp_fld); + return fill_column_size_buff(str_buff, buff_size, stmt, &temp_fld); } else { @@ -3741,7 +3700,8 @@ SQLLEN proc_get_param_col_len(STMT *stmt, int sql_type_index, SQLULEN col_size, Returns parameter octet length */ SQLLEN proc_get_param_octet_len(STMT *stmt, int sql_type_index, SQLULEN col_size, - SQLSMALLINT decimal_digits, unsigned int flags, char * str_buff) + SQLSMALLINT decimal_digits, unsigned int flags, + char * str_buff, size_t buff_size) { MYSQL_FIELD temp_fld; @@ -3757,7 +3717,7 @@ SQLLEN proc_get_param_octet_len(STMT *stmt, int sql_type_index, SQLULEN col_size if (str_buff != NULL) { - return fill_transfer_oct_len_buff(str_buff, stmt, &temp_fld); + return fill_transfer_oct_len_buff(str_buff, buff_size, stmt, &temp_fld); } else { @@ -3858,7 +3818,7 @@ void set_row_count(STMT *stmt, my_ulonglong rows) if (stmt != NULL && stmt->result != NULL) { stmt->result->row_count= rows; - stmt->dbc->mysql->affected_rows= rows; + stmt->dbc->mysql_proxy->set_affected_rows(rows); } } @@ -4337,7 +4297,7 @@ int got_out_parameters(STMT *stmt) } -int get_session_variable(STMT *stmt, const char *var, char *result) +int get_session_variable(STMT *stmt, const char *var, char *result, size_t result_size) { char buff[255+4*NAME_CHAR_LEN], *to; MYSQL_RES *res; @@ -4355,19 +4315,19 @@ int get_session_variable(STMT *stmt, const char *var, char *result) return 0; } - res= mysql_store_result(stmt->dbc->mysql); + res = stmt->dbc->mysql_proxy->store_result(); if (!res) return 0; - row= mysql_fetch_row(res); + row = stmt->dbc->mysql_proxy->fetch_row(res); if (row) { - strcpy(result, row[1]); - mysql_free_result(res); + strncpy(result, row[1], result_size); + stmt->dbc->mysql_proxy->free_result(res); return strlen(result); } - mysql_free_result(res); + stmt->dbc->mysql_proxy->free_result(res); } return 0; @@ -4388,7 +4348,7 @@ SQLRETURN set_query_timeout(STMT *stmt, SQLULEN new_value) SQLRETURN rc= SQL_SUCCESS; if (new_value == stmt->stmt_options.query_timeout || - !is_minimum_version(stmt->dbc->mysql->server_version, "5.7.8")) + !is_minimum_version(stmt->dbc->mysql_proxy->get_server_version(), "5.7.8")) { /* Do nothing if setting same timeout or MySQL server older than 5.7.8 */ return SQL_SUCCESS; @@ -4397,11 +4357,11 @@ SQLRETURN set_query_timeout(STMT *stmt, SQLULEN new_value) if (new_value > 0) { unsigned long long msec_value= (unsigned long long)new_value * 1000; - sprintf(query, "set @@max_execution_time=%llu", msec_value); + snprintf(query, sizeof(query), "set @@max_execution_time=%llu", msec_value); } else { - strcpy(query, "set @@max_execution_time=DEFAULT"); + strncpy(query, "set @@max_execution_time=DEFAULT", sizeof(query)); new_value= 0; } @@ -4418,12 +4378,12 @@ SQLULEN get_query_timeout(STMT *stmt) { SQLULEN query_timeout= SQL_QUERY_TIMEOUT_DEFAULT; /* 0 */ - if (is_minimum_version(stmt->dbc->mysql->server_version, "5.7.8")) + if (is_minimum_version(stmt->dbc->mysql_proxy->get_server_version(), "5.7.8")) { /* Be cautious with very long values even if they don't make sense */ char query_timeout_char[32]= {0}; uint length= get_session_variable(stmt, "MAX_EXECUTION_TIME", - (char*)query_timeout_char); + (char*)query_timeout_char, sizeof(query_timeout_char)); /* Terminate the string just in case */ query_timeout_char[length]= 0; /* convert */ @@ -4437,7 +4397,7 @@ const char get_identifier_quote(STMT *stmt) { const char tick= '`', quote= '"', empty= ' '; - if (is_minimum_version(stmt->dbc->mysql->server_version, "3.23.06")) + if (is_minimum_version(stmt->dbc->mysql_proxy->get_server_version(), "3.23.06")) { /* The full list of all SQL modes takes over 512 symbols, so we reserve @@ -4448,7 +4408,7 @@ const char get_identifier_quote(STMT *stmt) The token finder skips the leading space and starts with the first non-space value. Thus (sql_mode+1). */ - uint length= get_session_variable(stmt, "SQL_MODE", (char*)(sql_mode+1)); + uint length= get_session_variable(stmt, "SQL_MODE", (char*)(sql_mode+1), sizeof(sql_mode)-1); const char *end= sql_mode + length; if (find_first_token(stmt->dbc->ansi_charset_info, sql_mode, end, "ANSI_QUOTES")) diff --git a/installer/myodbc-installer.cc b/installer/myodbc-installer.cc index cfa3d105..b0282acc 100644 --- a/installer/myodbc-installer.cc +++ b/installer/myodbc-installer.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -236,8 +238,6 @@ void print_odbc_error(SQLHANDLE hnd, SQLSMALLINT type) */ int list_driver_details(Driver *driver) { - SQLWCHAR buf[50000]; - SQLWCHAR *entries= buf; int rc; /* lookup the driver */ @@ -518,6 +518,10 @@ int list_datasource_details(DataSource *ds) if (ds->plugin_dir ) printf("Plugin directory: %s\n", ds_get_utf8attr(ds->plugin_dir, &ds->plugin_dir8)); if (ds->default_auth) printf("Default Authentication Library: %s\n", ds_get_utf8attr(ds->default_auth, &ds->default_auth8)); if (ds->oci_config_file) printf("OCI Config File: %s\n", ds_get_utf8attr(ds->oci_config_file, &ds->oci_config_file8)); + /* Failover */ + if (ds->host_pattern) printf("Failover Instance Host pattern: %s\n", ds_get_utf8attr(ds->host_pattern, &ds->host_pattern8)); + if (ds->cluster_id) printf("Failover Cluster ID: %s\n", ds_get_utf8attr(ds->cluster_id, &ds->cluster_id8)); + printf("Options:\n"); if (ds->return_matching_rows) printf("\tFOUND_ROWS\n"); if (ds->allow_big_results) printf("\tBIG_PACKETS\n"); @@ -540,7 +544,7 @@ int list_datasource_details(DataSource *ds) if (ds->dont_cache_result) printf("\tNO_CACHE\n"); if (ds->force_use_of_forward_only_cursors) printf("\tFORWARD_CURSOR\n"); if (ds->auto_reconnect) printf("\tAUTO_RECONNECT\n"); - if (ds->clientinteractive) printf("\tINTERACTIVE\n"); + if (ds->client_interactive) printf("\tINTERACTIVE\n"); if (ds->auto_increment_null_search) printf("\tAUTO_IS_NULL\n"); if (ds->zero_date_to_min) printf("\tZERO_DATE_TO_MIN\n"); if (ds->min_date_to_zero) printf("\tMIN_DATE_TO_ZERO\n"); @@ -554,13 +558,32 @@ int list_datasource_details(DataSource *ds) if (ds->no_tls_1_3) printf("\tNO_TLS_1_3\n"); if (ds->no_ssps) printf("\tNO_SSPS\n"); if (ds->cursor_prefetch_number) printf("\tPREFETCH=%d\n", ds->cursor_prefetch_number); - if (ds->readtimeout) printf("\tREADTIMEOUT=%d\n", ds->readtimeout); - if (ds->writetimeout) printf("\tWRITETIMEOUT=%d\n", ds->writetimeout); + if (ds->read_timeout) printf("\tREAD_TIMEOUT=%d\n", ds->read_timeout); + if (ds->write_timeout) printf("\tWRITE_TIMEOUT=%d\n", ds->write_timeout); if (ds->can_handle_exp_pwd) printf("\tCAN_HANDLE_EXP_PWD\n"); if (ds->enable_cleartext_plugin) printf("\tENABLE_CLEARTEXT_PLUGIN\n"); if (ds->get_server_public_key) printf("\tGET_SERVER_PUBLIC_KEY\n"); if (ds->enable_dns_srv) printf("\tENABLE_DNS_SRV\n"); if (ds->multi_host) printf("\tMULTI_HOST\n"); + /* Failover */ + if (ds->enable_cluster_failover) printf("\tENABLE_CLUSTER_FAILOVER\n"); + if (ds->allow_reader_connections) printf("\tALLOW_READER_CONNECTIONS\n"); + if (ds->gather_perf_metrics) printf("\tGATHER_PERF_METRICS\n"); + if (ds->gather_metrics_per_instance) printf("\tGATHER_METRICS_PER_INSTANCE\n"); + if (ds->topology_refresh_rate) printf("\tTOPOLOGY_REFRESH_RATE=%d\n", ds->topology_refresh_rate); + if (ds->failover_timeout) printf("\tFAILOVER_TIMEOUT=%d\n", ds->failover_timeout); + if (ds->failover_topology_refresh_rate) printf("\tFAILOVER_TOPOLOGY_REFRESH_RATE=%d\n", ds->failover_topology_refresh_rate); + if (ds->failover_writer_reconnect_interval) printf("\tFAILOVER_WRITER_RECONNECT_INTERVAL=%d\n", ds->failover_writer_reconnect_interval); + if (ds->failover_reader_connect_timeout) printf("\tFAILOVER_READER_CONNECT_TIMEOUT=%d\n", ds->failover_reader_connect_timeout); + if (ds->connect_timeout) printf("\tCONNECT_TIMEOUT=%d\n", ds->connect_timeout); + if (ds->network_timeout) printf("\tNETWORK_TIMEOUT=%d\n", ds->network_timeout); + /* Monitoring */ + if (ds->enable_failure_detection) printf("\tENABLE_FAILURE_DETECTION\n"); + if (ds->failure_detection_time) printf("\tFAILURE_DETECTION_TIME=%d\n", ds->failure_detection_time); + if (ds->failure_detection_timeout) printf("\tFAILURE_DETECTION_TIMEOUT=%d\n", ds->failure_detection_timeout); + if (ds->failure_detection_interval) printf("\tFAILURE_DETECTION_INTERVAL=%d\n", ds->failure_detection_interval); + if (ds->failure_detection_count) printf("\tFAILURE_DETECTION_COUNT=%d\n", ds->failure_detection_count); + if (ds->monitor_disposal_time) printf("\tMONITOR_DISPOSAL_TIME=%d\n", ds->monitor_disposal_time); return 0; } @@ -574,7 +597,7 @@ int list_datasources() SQLHANDLE env; SQLRETURN rc; SQLUSMALLINT dir= 0; /* SQLDataSources fetch direction */ - SQLCHAR name[256]; + SQLCHAR server_name[256]; SQLCHAR description[256]; /* determine 'direction' to pass to SQLDataSources */ @@ -608,10 +631,10 @@ int list_datasources() } /* retrieve and print data source */ - while ((rc= SQLDataSources(env, dir, name, 256, NULL, description, + while ((rc= SQLDataSources(env, dir, server_name, 256, NULL, description, 256, NULL)) == SQL_SUCCESS) { - printf("%-20s - %s\n", name, description); + printf("%-20s - %s\n", server_name, description); dir= SQL_FETCH_NEXT; } @@ -715,7 +738,7 @@ int handle_datasource_action() /* set name if given */ if (name) - ds_set_strattr(&ds->name, wname); + ds_set_wstrattr(&ds->name, wname); /* perform given action */ switch (action) diff --git a/integration/CMakeLists.txt b/integration/CMakeLists.txt new file mode 100644 index 00000000..7f34042c --- /dev/null +++ b/integration/CMakeLists.txt @@ -0,0 +1,138 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0 +# (GPLv2), as published by the Free Software Foundation, with the +# following additional permissions: +# +# This program is distributed with certain software that is licensed +# under separate terms, as designated in a particular file or component +# or in the license documentation. Without limiting your rights under +# the GPLv2, the authors of this program hereby grant you an additional +# permission to link the program and your derivative works with the +# separately licensed software that they have included with the program. +# +# Without limiting the foregoing grant of rights under the GPLv2 and +# additional permission as to separately licensed software, this +# program is also subject to the Universal FOSS Exception, version 1.0, +# a copy of which can be found along with its FAQ at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# http://www.gnu.org/licenses/gpl-2.0.html. + +cmake_minimum_required(VERSION 3.14) + +project(integration) + +# GoogleTest requires at least C++11 +set(CMAKE_CXX_STANDARD 17) + +#-------------- unixodbc/iodbc/win ------------------- +if(WIN32) + set(ODBCLIB odbc32) +else(WIN32) + if(WITH_UNIXODBC) + set(ODBCLIB odbc) + else(WITH_UNIXODBC) + set(ODBCLIB iodbc) + endif(WITH_UNIXODBC) +endif(WIN32) + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip +) +FetchContent_Declare( + awssdk + GIT_REPOSITORY https://github.com/aws/aws-sdk-cpp +) +FetchContent_Declare( + toxiproxy + GIT_REPOSITORY https://github.com/Bit-Quill/toxiproxy-cpp + GIT_TAG main +) +FetchContent_Declare( + openxlsx + GIT_REPOSITORY https://github.com/troldal/OpenXLSX +) + +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +# Only builds the RDS SDK +set(BUILD_ONLY "rds" CACHE INTERNAL "") +set(ENABLE_TESTING OFF CACHE BOOL "" FORCE) + +# OpenXLSX +set(OPENXLSX_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(OPENXLSX_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) +set(OPENXLSX_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) +set(OPENXLSX_CREATE_DOCS OFF CACHE BOOL "" FORCE) + +set(CMAKE_EXE_LINKER_FLAGS_DEBUGOPT + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE) +set(CMAKE_SHARED_LINKER_FLAGS_DEBUGOPT + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE) +mark_as_advanced( + CMAKE_CXX_FLAGS_DEBUGOPT + CMAKE_C_FLAGS_DEBUGOPT + CMAKE_EXE_LINKER_FLAGS_DEBUGOPT + CMAKE_SHARED_LINKER_FLAGS_DEBUGOPT) +set(AWS_SDK_DEPENDENCIES aws-c-auth aws-c-cal aws-c-common aws-c-compression + aws-c-event-stream aws-checksums aws-c-http aws-c-io aws-c-mqtt + aws-crt-cpp aws-c-s3 aws-cpp-sdk-core aws-cpp-sdk-rds) + +FetchContent_MakeAvailable(googletest toxiproxy awssdk) + +if(ENABLE_PERFORMANCE_TESTS) + FetchContent_MakeAvailable(openxlsx) +endif() + +enable_testing() + +set(TEST_SOURCES connection_string_builder.cc base_failover_integration_test.cc connection_string_builder_test.cc) +set(INTEGRATION_TESTS network_failover_integration_test.cc failover_integration_test.cc) + +if(NOT ENABLE_PERFORMANCE_TESTS) + set(TEST_SOURCES ${TEST_SOURCES} ${INTEGRATION_TESTS}) +else() + set(TEST_SOURCES ${TEST_SOURCES} failover_performance_test.cc) +endif() + +add_executable( + integration + ${TEST_SOURCES} +) + +include(GoogleTest) + +gtest_discover_tests(integration) + +target_include_directories(integration PUBLIC "${httplib_SOURCE_DIR}") + +target_link_libraries( + integration + nlohmann_json::nlohmann_json + ${ODBCLIB} + gtest_main + toxiproxy + ${AWS_SDK_DEPENDENCIES} +) + +if(ENABLE_PERFORMANCE_TESTS) + target_link_libraries(integration OpenXLSX::OpenXLSX) +endif() + +set_target_properties(integration PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) diff --git a/integration/base_failover_integration_test.cc b/integration/base_failover_integration_test.cc new file mode 100644 index 00000000..264ac3b3 --- /dev/null +++ b/integration/base_failover_integration_test.cc @@ -0,0 +1,472 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +// Those classes need to be included first in Windows +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "connection_string_builder.cc" + +#define MAX_NAME_LEN 255 +#define SQL_MAX_MESSAGE_LENGTH 512 + +#define AS_SQLCHAR(str) const_cast(reinterpret_cast(str)) + +static int str_to_int(const char* str) { + const long int x = strtol(str, nullptr, 10); + assert(x <= INT_MAX); + assert(x >= INT_MIN); + return static_cast(x); +} + +static std::string DOWN_STREAM_STR = "DOWNSTREAM"; +static std::string UP_STREAM_STR = "UPSTREAM"; + +static Aws::SDKOptions options; + +class BaseFailoverIntegrationTest : public testing::Test { +protected: + // Connection string parameters + char* dsn = std::getenv("TEST_DSN"); + char* db = std::getenv("TEST_DATABASE"); + char* user = std::getenv("TEST_UID"); + char* pwd = std::getenv("TEST_PASSWORD"); + + std::string MYSQL_INSTANCE_1_URL = std::getenv("MYSQL_INSTANCE_1_URL"); + std::string MYSQL_INSTANCE_2_URL = std::getenv("MYSQL_INSTANCE_2_URL"); + std::string MYSQL_INSTANCE_3_URL = std::getenv("MYSQL_INSTANCE_3_URL"); + std::string MYSQL_INSTANCE_4_URL = std::getenv("MYSQL_INSTANCE_4_URL"); + std::string MYSQL_INSTANCE_5_URL = std::getenv("MYSQL_INSTANCE_5_URL"); + std::string MYSQL_CLUSTER_URL = std::getenv("TEST_SERVER"); + std::string MYSQL_RO_CLUSTER_URL = std::getenv("TEST_RO_SERVER"); + + std::string PROXIED_DOMAIN_NAME_SUFFIX = std::getenv("PROXIED_DOMAIN_NAME_SUFFIX"); + std::string PROXIED_CLUSTER_TEMPLATE = std::getenv("PROXIED_CLUSTER_TEMPLATE"); + std::string DB_CONN_STR_SUFFIX = std::getenv("DB_CONN_STR_SUFFIX"); + + int MYSQL_PORT = str_to_int(std::getenv("MYSQL_PORT")); + int MYSQL_PROXY_PORT = str_to_int(std::getenv("MYSQL_PROXY_PORT")); + Aws::String cluster_id = MYSQL_CLUSTER_URL.substr(0, MYSQL_CLUSTER_URL.find('.')); + + static const int GLOBAL_FAILOVER_TIMEOUT = 120000; + + ConnectionStringBuilder builder; + std::string connection_string; + + SQLCHAR conn_in[4096] = "\0", conn_out[4096] = "\0", sqlstate[6] = "\0", message[SQL_MAX_MESSAGE_LENGTH] = "\0"; + SQLINTEGER native_error = 0; + SQLSMALLINT len = 0, length = 0; + + std::string TOXIPROXY_INSTANCE_1_NETWORK_ALIAS = std::getenv("TOXIPROXY_INSTANCE_1_NETWORK_ALIAS"); + std::string TOXIPROXY_INSTANCE_2_NETWORK_ALIAS = std::getenv("TOXIPROXY_INSTANCE_2_NETWORK_ALIAS"); + std::string TOXIPROXY_INSTANCE_3_NETWORK_ALIAS = std::getenv("TOXIPROXY_INSTANCE_3_NETWORK_ALIAS"); + std::string TOXIPROXY_INSTANCE_4_NETWORK_ALIAS = std::getenv("TOXIPROXY_INSTANCE_4_NETWORK_ALIAS"); + std::string TOXIPROXY_INSTANCE_5_NETWORK_ALIAS = std::getenv("TOXIPROXY_INSTANCE_5_NETWORK_ALIAS"); + std::string TOXIPROXY_CLUSTER_NETWORK_ALIAS = std::getenv("TOXIPROXY_CLUSTER_NETWORK_ALIAS"); + std::string TOXIPROXY_RO_CLUSTER_NETWORK_ALIAS = std::getenv("TOXIPROXY_RO_CLUSTER_NETWORK_ALIAS"); + + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_client_instance_1 = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_INSTANCE_1_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_client_instance_2 = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_INSTANCE_2_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_client_instance_3 = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_INSTANCE_3_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_client_instance_4 = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_INSTANCE_4_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_client_instance_5 = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_INSTANCE_5_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_cluster = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_CLUSTER_NETWORK_ALIAS); + TOXIPROXY::TOXIPROXY_CLIENT* toxiproxy_read_only_cluster = new TOXIPROXY::TOXIPROXY_CLIENT(TOXIPROXY_RO_CLUSTER_NETWORK_ALIAS); + + TOXIPROXY::PROXY* proxy_instance_1 = get_proxy(toxiproxy_client_instance_1, MYSQL_INSTANCE_1_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_instance_2 = get_proxy(toxiproxy_client_instance_2, MYSQL_INSTANCE_2_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_instance_3 = get_proxy(toxiproxy_client_instance_3, MYSQL_INSTANCE_3_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_instance_4 = get_proxy(toxiproxy_client_instance_4, MYSQL_INSTANCE_4_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_instance_5 = get_proxy(toxiproxy_client_instance_5, MYSQL_INSTANCE_5_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_cluster = get_proxy(toxiproxy_cluster, MYSQL_CLUSTER_URL, MYSQL_PORT); + TOXIPROXY::PROXY* proxy_read_only_cluster = get_proxy(toxiproxy_read_only_cluster, MYSQL_RO_CLUSTER_URL, MYSQL_PORT); + + std::map proxy_map = { + {MYSQL_INSTANCE_1_URL.substr(0, MYSQL_INSTANCE_1_URL.find('.')), proxy_instance_1}, + {MYSQL_INSTANCE_2_URL.substr(0, MYSQL_INSTANCE_2_URL.find('.')), proxy_instance_2}, + {MYSQL_INSTANCE_3_URL.substr(0, MYSQL_INSTANCE_3_URL.find('.')), proxy_instance_3}, + {MYSQL_INSTANCE_4_URL.substr(0, MYSQL_INSTANCE_4_URL.find('.')), proxy_instance_4}, + {MYSQL_INSTANCE_5_URL.substr(0, MYSQL_INSTANCE_5_URL.find('.')), proxy_instance_5}, + {MYSQL_CLUSTER_URL, proxy_cluster}, + {MYSQL_RO_CLUSTER_URL, proxy_read_only_cluster} + }; + + std::vector cluster_instances; + std::string writer_id; + std::string writer_endpoint; + std::vector readers; + std::string reader_id; + std::string reader_endpoint; + + // Queries + SQLCHAR* SERVER_ID_QUERY = AS_SQLCHAR("SELECT @@aurora_server_id"); + + // Error codes + const std::string ERROR_COMM_LINK_FAILURE = "08S01"; + const std::string ERROR_COMM_LINK_CHANGED = "08S02"; + const std::string ERROR_CONN_FAILURE_DURING_TX = "08007"; + + // Helper functions + + std::string get_endpoint(const std::string& instance_id) const { + return instance_id + DB_CONN_STR_SUFFIX; + } + + std::string get_proxied_endpoint(const std::string& instance_id) const { + return instance_id + DB_CONN_STR_SUFFIX + PROXIED_DOMAIN_NAME_SUFFIX; + } + + static std::string get_writer_id(std::vector instances) { + if (instances.empty()) { + throw std::runtime_error("The input cluster topology is empty."); + } + return instances[0]; + } + + static std::vector get_readers(std::vector instances) { + if (instances.size() < 2) { + throw std::runtime_error("The input cluster topology does not contain a reader."); + } + const std::vector::const_iterator first_reader = instances.begin() + 1; + const std::vector::const_iterator last_reader = instances.end(); + std::vector readers_list(first_reader, last_reader); + return readers_list; + } + + static std::string get_first_reader_id(std::vector instances) { + if (instances.size() < 2) { + throw std::runtime_error("The input cluster topology does not contain a reader."); + } + return instances[1]; + } + + void assert_query_succeeded(const SQLHDBC dbc, SQLCHAR* query) const { + SQLHSTMT handle; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLFreeHandle(SQL_HANDLE_STMT, handle)); + } + + void assert_query_failed(const SQLHDBC dbc, SQLCHAR* query, const std::string& expected_error) const { + SQLHSTMT handle; + SQLSMALLINT stmt_length; + SQLINTEGER native_err; + SQLCHAR msg[SQL_MAX_MESSAGE_LENGTH] = "\0", state[6] = "\0"; + + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + EXPECT_EQ(SQL_ERROR, SQLExecDirect(handle, query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLError(nullptr, nullptr, handle, state, &native_err, msg, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length)); + const std::string state_str = reinterpret_cast(state); + EXPECT_EQ(expected_error, state_str); + EXPECT_EQ(SQL_SUCCESS, SQLFreeHandle(SQL_HANDLE_STMT, handle)); + } + + std::string query_instance_id(const SQLHDBC dbc) const { + SQLCHAR buf[255] = "\0"; + SQLLEN buflen; + SQLHSTMT handle; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, SERVER_ID_QUERY, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLFetch(handle)); + EXPECT_EQ(SQL_SUCCESS, SQLGetData(handle, 1, SQL_CHAR, buf, sizeof(buf), &buflen)); + EXPECT_EQ(SQL_SUCCESS, SQLFreeHandle(SQL_HANDLE_STMT, handle)); + std::string id(reinterpret_cast(buf)); + return id; + } + + // Helper functions from integration tests + + static std::vector retrieve_topology_via_SDK(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id) { + std::vector instances; + + std::string writer; + std::vector readers; + + Aws::RDS::Model::DescribeDBClustersRequest rds_req; + rds_req.WithDBClusterIdentifier(cluster_id); + auto outcome = client.DescribeDBClusters(rds_req); + + if (!outcome.IsSuccess()) { + throw std::runtime_error("Unable to get cluster topology using SDK."); + } + + const auto result = outcome.GetResult(); + const Aws::RDS::Model::DBCluster cluster = result.GetDBClusters()[0]; + + for (const auto& instance : cluster.GetDBClusterMembers()) { + std::string instance_id(instance.GetDBInstanceIdentifier()); + if (instance.GetIsClusterWriter()) { + writer = instance_id; + } else { + readers.push_back(instance_id); + } + } + + instances.push_back(writer); + for (const auto& reader : readers) { + instances.push_back(reader); + } + return instances; + } + + static Aws::RDS::Model::DBCluster get_DB_cluster(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id) { + Aws::RDS::Model::DescribeDBClustersRequest rds_req; + rds_req.WithDBClusterIdentifier(cluster_id); + auto outcome = client.DescribeDBClusters(rds_req); + const auto result = outcome.GetResult(); + return result.GetDBClusters().at(0); + } + + static void wait_until_cluster_has_right_state(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id) { + Aws::String status = get_DB_cluster(client, cluster_id).GetStatus(); + + while (status != "available") { + std::this_thread::sleep_for(std::chrono::seconds(1)); + status = get_DB_cluster(client, cluster_id).GetStatus(); + } + } + + static Aws::RDS::Model::DBClusterMember get_DB_cluster_writer_instance(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id) { + Aws::RDS::Model::DBClusterMember instance; + const Aws::RDS::Model::DBCluster cluster = get_DB_cluster(client, cluster_id); + for (const auto& member : cluster.GetDBClusterMembers()) { + if (member.GetIsClusterWriter()) { + return member; + } + } + return instance; + } + + static Aws::String get_DB_cluster_writer_instance_id(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id) { + return get_DB_cluster_writer_instance(client, cluster_id).GetDBInstanceIdentifier(); + } + + static void wait_until_writer_instance_changed(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, + const Aws::String& initial_writer_instance_id) { + Aws::String next_cluster_writer_id = get_DB_cluster_writer_instance_id(client, cluster_id); + while (initial_writer_instance_id == next_cluster_writer_id) { + std::this_thread::sleep_for(std::chrono::seconds(3)); + next_cluster_writer_id = get_DB_cluster_writer_instance_id(client, cluster_id); + } + } + + static void failover_cluster(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, const Aws::String& target_instance_id = "") { + wait_until_cluster_has_right_state(client, cluster_id); + Aws::RDS::Model::FailoverDBClusterRequest rds_req; + rds_req.WithDBClusterIdentifier(cluster_id); + if (!target_instance_id.empty()) { + rds_req.WithTargetDBInstanceIdentifier(target_instance_id); + } + auto outcome = client.FailoverDBCluster(rds_req); + } + + static void failover_cluster_and_wait_until_writer_changed(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, + const Aws::String& cluster_writer_id, + const Aws::String& target_writer_id = "") { + failover_cluster(client, cluster_id, target_writer_id); + wait_until_writer_instance_changed(client, cluster_id, cluster_writer_id); + } + + static Aws::RDS::Model::DBClusterMember get_matched_DBClusterMember(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, + const Aws::String& instance_id) { + Aws::RDS::Model::DBClusterMember instance; + const Aws::RDS::Model::DBCluster cluster = get_DB_cluster(client, cluster_id); + for (const auto& member : cluster.GetDBClusterMembers()) { + Aws::String member_id = member.GetDBInstanceIdentifier(); + if (member_id == instance_id) { + return member; + } + } + return instance; + } + + static bool is_DB_instance_writer(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, const Aws::String& instance_id) { + return get_matched_DBClusterMember(client, cluster_id, instance_id).GetIsClusterWriter(); + } + + static bool is_DB_instance_reader(const Aws::RDS::RDSClient& client, const Aws::String& cluster_id, const Aws::String& instance_id) { + return !get_matched_DBClusterMember(client, cluster_id, instance_id).GetIsClusterWriter(); + } + + static int query_count_table_rows(const SQLHSTMT handle, const char* table_name, const int id = -1) { + EXPECT_FALSE(table_name[0] == '\0'); + + //TODO Investigate how to use Prepared Statements to protect against SQL injection + char select_count_query[256]; + if (id == -1) { + sprintf(select_count_query, "SELECT count(*) FROM %s", table_name); + } else { + sprintf(select_count_query, "SELECT count(*) FROM %s WHERE id = %d", table_name, id); + } + + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, AS_SQLCHAR(select_count_query), SQL_NTS)); + const auto rc = SQLFetch(handle); + + SQLINTEGER buf = -1; + SQLLEN buflen; + if (rc != SQL_NO_DATA_FOUND && rc != SQL_ERROR) { + EXPECT_EQ(SQL_SUCCESS, SQLGetData(handle, 1, SQL_INTEGER, &buf, sizeof(buf), &buflen)); + SQLFetch(handle); // To get cursor in correct position + } + return buf; + } + + // Helper functions from network integration tests + + static TOXIPROXY::PROXY* get_proxy(TOXIPROXY::TOXIPROXY_CLIENT* client, const std::string& host, const int port) { + const std::string upstream = host + ":" + std::to_string(port); + return client->get_proxy(upstream); + } + + TOXIPROXY::PROXY* get_proxy_from_map(const std::string& url) { + const auto it = proxy_map.find(url); + if (it != proxy_map.end()) { + return it->second; + } + return nullptr; + } + + bool is_db_instance_writer(const std::string& instance) const { + return writer_id == instance; + } + + bool is_db_instance_reader(const std::string& instance) const { + for (const auto& reader : readers) { + if (reader == instance) { + return true; + } + } + return false; + } + + void disable_instance(const std::string& instance) { + TOXIPROXY::PROXY* new_instance = get_proxy_from_map(instance); + if (new_instance) { + disable_connectivity(new_instance); + } else { + FAIL() << instance << " does not have a proxy setup."; + } + } + + static void disable_connectivity(const TOXIPROXY::PROXY* proxy) { + const auto toxics = proxy->get_toxics(); + if (toxics) { + toxics->bandwidth(DOWN_STREAM_STR, TOXIPROXY::TOXIC_DIRECTION::DOWNSTREAM, 0); + toxics->bandwidth(UP_STREAM_STR, TOXIPROXY::TOXIC_DIRECTION::UPSTREAM, 0); + } + } + + void enable_instance(const std::string& instance) { + TOXIPROXY::PROXY* new_instance = get_proxy_from_map(instance); + if (new_instance) { + enable_connectivity(new_instance); + } else { + FAIL() << instance << " does not have a proxy setup."; + } + } + + static void enable_connectivity(const TOXIPROXY::PROXY* proxy) { + TOXIPROXY::TOXIC_LIST* toxics = proxy->get_toxics(); + + if (toxics) { + TOXIPROXY::TOXIC* downstream = toxics->get(DOWN_STREAM_STR); + TOXIPROXY::TOXIC* upstream = toxics->get(UP_STREAM_STR); + + if (downstream) { + downstream->remove(); + } + if (upstream) { + upstream->remove(); + } + } + } + + std::string get_default_config(const int connect_timeout = 10, const int network_timeout = 10) const { + char template_connection[4096]; + sprintf(template_connection, "DSN=%s;UID=%s;PWD=%s;LOG_QUERY=1;CONNECT_TIMEOUT=%d;NETWORK_TIMEOUT=%d;", dsn, user, pwd, connect_timeout, network_timeout); + std::string config(template_connection); + return config; + } + + std::string get_default_proxied_config() const { + char template_connection[4096]; + sprintf(template_connection, "%sHOST_PATTERN=%s;", get_default_config().c_str(), PROXIED_CLUSTER_TEMPLATE.c_str()); + std::string config(template_connection); + return config; + } + + void test_connection(const SQLHDBC dbc, const std::string& test_server, const int test_port) { + sprintf(reinterpret_cast(conn_in), "%sSERVER=%s;PORT=%d;", get_default_proxied_config().c_str(), test_server.c_str(), test_port); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, conn_in, SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); + } + + static void assert_is_new_reader(const std::vector& old_readers, const std::string& new_reader) { + for (const auto& reader : old_readers) { + EXPECT_NE(reader, new_reader); + } + } + +public: + ~BaseFailoverIntegrationTest() override { + delete toxiproxy_client_instance_1; + delete toxiproxy_client_instance_2; + delete toxiproxy_client_instance_3; + delete toxiproxy_client_instance_4; + delete toxiproxy_client_instance_5; + delete toxiproxy_cluster; + delete toxiproxy_read_only_cluster; + + delete proxy_instance_1; + delete proxy_instance_2; + delete proxy_instance_3; + delete proxy_instance_4; + delete proxy_instance_5; + delete proxy_cluster; + delete proxy_read_only_cluster; + } +}; diff --git a/integration/connection_string_builder.cc b/integration/connection_string_builder.cc new file mode 100644 index 00000000..d850a0ff --- /dev/null +++ b/integration/connection_string_builder.cc @@ -0,0 +1,386 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include +#include +#include + +class ConnectionStringBuilder; + +class ConnectionString { + public: + // friend class so the builder can access ConnectionString private attributes + friend class ConnectionStringBuilder; + + ConnectionString() : m_dsn(""), m_server(""), m_port(-1), + m_uid(""), m_pwd(""), m_db(""), m_log_query(true), m_allow_reader_connections(false), + m_multi_statements(false), m_enable_cluster_failover(true), m_failover_timeout(-1), + m_connect_timeout(-1), m_network_timeout(-1), m_host_pattern(""), + m_enable_failure_detection(true), m_failure_detection_time(-1), m_failure_detection_timeout(-1), + m_failure_detection_interval(-1), m_failure_detection_count(-1), m_monitor_disposal_time(-1), + m_read_timeout(-1), m_write_timeout(-1), + + is_set_uid(false), is_set_pwd(false), is_set_db(false), is_set_log_query(false), + is_set_allow_reader_connections(false), is_set_multi_statements(false), is_set_enable_cluster_failover(false), + is_set_failover_timeout(false), is_set_connect_timeout(false), is_set_network_timeout(false), is_set_host_pattern(false), + is_set_enable_failure_detection(false), is_set_failure_detection_time(false), is_set_failure_detection_timeout(false), + is_set_failure_detection_interval(false), is_set_failure_detection_count(false), is_set_monitor_disposal_time(false), + is_set_read_timeout(false), is_set_write_timeout(false) {}; + + std::string get_connection_string() const { + char conn_in[4096] = "\0"; + int length = 0; + length += sprintf(conn_in, "DSN=%s;SERVER=%s;PORT=%d;", m_dsn.c_str(), m_server.c_str(), m_port); + + if (is_set_uid) { + length += sprintf(conn_in + length, "UID=%s;", m_uid.c_str()); + } + if (is_set_pwd) { + length += sprintf(conn_in + length, "PWD=%s;", m_pwd.c_str()); + } + if (is_set_db) { + length += sprintf(conn_in + length, "DATABASE=%s;", m_db.c_str()); + } + if (is_set_log_query) { + length += sprintf(conn_in + length, "LOG_QUERY=%d;", m_log_query ? 1 : 0); + } + if (is_set_allow_reader_connections) { + length += sprintf(conn_in + length, "ALLOW_READER_CONNECTIONS=%d;", m_allow_reader_connections ? 1 : 0); + } + if (is_set_multi_statements) { + length += sprintf(conn_in + length, "MULTI_STATEMENTS=%d;", m_multi_statements ? 1 : 0); + } + if (is_set_enable_cluster_failover) { + length += sprintf(conn_in + length, "ENABLE_CLUSTER_FAILOVER=%d;", m_enable_cluster_failover ? 1 : 0); + } + if (is_set_failover_timeout) { + length += sprintf(conn_in + length, "FAILOVER_TIMEOUT=%d;", m_failover_timeout); + } + if (is_set_connect_timeout) { + length += sprintf(conn_in + length, "CONNECT_TIMEOUT=%d;", m_connect_timeout); + } + if (is_set_network_timeout) { + length += sprintf(conn_in + length, "NETWORK_TIMEOUT=%d;", m_network_timeout); + } + if (is_set_host_pattern) { + length += sprintf(conn_in + length, "HOST_PATTERN=%s;", m_host_pattern.c_str()); + } + if (is_set_enable_failure_detection) { + length += sprintf(conn_in + length, "ENABLE_FAILURE_DETECTION=%d;", m_enable_failure_detection ? 1 : 0); + } + if (is_set_failure_detection_time) { + length += sprintf(conn_in + length, "FAILURE_DETECTION_TIME=%d;", m_failure_detection_time); + } + if (is_set_failure_detection_timeout) { + length += sprintf(conn_in + length, "FAILURE_DETECTION_TIMEOUT=%d;", m_failure_detection_timeout); + } + if (is_set_failure_detection_interval) { + length += sprintf(conn_in + length, "FAILURE_DETECTION_INTERVAL=%d;", m_failure_detection_interval); + } + if (is_set_failure_detection_count) { + length += sprintf(conn_in + length, "FAILURE_DETECTION_COUNT=%d;", m_failure_detection_count); + } + if (is_set_monitor_disposal_time) { + length += sprintf(conn_in + length, "MONITOR_DISPOSAL_TIME=%d;", m_monitor_disposal_time); + } + if (is_set_read_timeout) { + length += sprintf(conn_in + length, "READTIMEOUT=%d;", m_read_timeout); + } + if (is_set_write_timeout) { + length += sprintf(conn_in + length, "WRITETIMEOUT=%d;", m_write_timeout); + } + snprintf(conn_in + length, sizeof(conn_in) - length, "\0"); + + std::string connection_string(conn_in); + return connection_string; + } + + private: + // Required fields + std::string m_dsn, m_server; + int m_port; + + // Optional fields + std::string m_uid, m_pwd, m_db; + bool m_log_query, m_allow_reader_connections, m_multi_statements, m_enable_cluster_failover; + int m_failover_timeout, m_connect_timeout, m_network_timeout; + std::string m_host_pattern; + bool m_enable_failure_detection; + int m_failure_detection_time, m_failure_detection_timeout, m_failure_detection_interval, m_failure_detection_count, m_monitor_disposal_time, m_read_timeout, m_write_timeout; + + bool is_set_uid, is_set_pwd, is_set_db; + bool is_set_log_query, is_set_allow_reader_connections, is_set_multi_statements; + bool is_set_enable_cluster_failover; + bool is_set_failover_timeout, is_set_connect_timeout, is_set_network_timeout; + bool is_set_host_pattern; + bool is_set_enable_failure_detection; + bool is_set_failure_detection_time, is_set_failure_detection_timeout, is_set_failure_detection_interval, is_set_failure_detection_count; + bool is_set_monitor_disposal_time; + bool is_set_read_timeout, is_set_write_timeout; + + void set_dsn(const std::string& dsn) { + m_dsn = dsn; + } + + void set_server(const std::string& server) { + m_server = server; + } + + void set_port(const int& port) { + m_port = port; + } + + void set_uid(const std::string& uid) { + m_uid = uid; + is_set_uid = true; + } + + void set_pwd(const std::string& pwd) { + m_pwd = pwd; + is_set_pwd = true; + } + + void set_db(const std::string& db) { + m_db = db; + is_set_db = true; + } + + void set_log_query(const bool& log_query) { + m_log_query = log_query; + is_set_log_query = true; + } + + void set_allow_reader_connections(const bool& allow_reader_connections) { + m_allow_reader_connections = allow_reader_connections; + is_set_allow_reader_connections = true; + } + + void set_multi_statements(const bool& multi_statements) { + m_multi_statements = multi_statements; + is_set_multi_statements = true; + } + + void set_enable_cluster_failover(const bool& enable_cluster_failover) { + m_enable_cluster_failover = enable_cluster_failover; + is_set_enable_cluster_failover = true; + } + + void set_failover_timeout(const int& failover_timeout) { + m_failover_timeout = failover_timeout; + is_set_failover_timeout = true; + } + + void set_connect_timeout(const int& connect_timeout) { + m_connect_timeout = connect_timeout; + is_set_connect_timeout = true; + } + + void set_network_timeout(const int& network_timeout) { + m_network_timeout = network_timeout; + is_set_network_timeout = true; + } + + void set_host_pattern(const std::string& host_pattern) { + m_host_pattern = host_pattern; + is_set_host_pattern = true; + } + + void set_enable_failure_detection(const bool& enable_failure_detection) { + m_enable_failure_detection = enable_failure_detection; + is_set_enable_failure_detection = true; + } + + void set_failure_detection_time(const int& failure_detection_time) { + m_failure_detection_time = failure_detection_time; + is_set_failure_detection_time = true; + } + + void set_failure_detection_timeout(const int& failure_detection_timeout) { + m_failure_detection_timeout = failure_detection_timeout; + is_set_failure_detection_timeout = true; + } + + void set_failure_detection_interval(const int& failure_detection_interval) { + m_failure_detection_interval = failure_detection_interval; + is_set_failure_detection_interval = true; + } + + void set_failure_detection_count(const int& failure_detection_count) { + m_failure_detection_count = failure_detection_count; + is_set_failure_detection_count = true; + } + + void set_monitor_disposal_time(const int& monitor_disposal_time) { + m_monitor_disposal_time = monitor_disposal_time; + is_set_monitor_disposal_time = true; + } + + void set_read_timeout(const int& read_timeout) { + m_read_timeout = read_timeout; + is_set_read_timeout = true; + } + + void set_write_timeout(const int& write_timeout) { + m_write_timeout = write_timeout; + is_set_write_timeout = true; + } +}; + +class ConnectionStringBuilder { + public: + ConnectionStringBuilder() { + connection_string.reset(new ConnectionString()); + } + + ConnectionStringBuilder& withDSN(const std::string& dsn) { + connection_string->set_dsn(dsn); + return *this; + } + + ConnectionStringBuilder& withServer(const std::string& server) { + connection_string->set_server(server); + return *this; + } + + ConnectionStringBuilder& withPort(const int& port) { + connection_string->set_port(port); + return *this; + } + + ConnectionStringBuilder& withUID(const std::string& uid) { + connection_string->set_uid(uid); + return *this; + } + + ConnectionStringBuilder& withPWD(const std::string& pwd) { + connection_string->set_pwd(pwd); + return *this; + } + + ConnectionStringBuilder& withDatabase(const std::string& db) { + connection_string->set_db(db); + return *this; + } + + ConnectionStringBuilder& withLogQuery(const bool& log_query) { + connection_string->set_log_query(log_query); + return *this; + } + + ConnectionStringBuilder& withAllowReaderConnections(const bool& allow_reader_connections) { + connection_string->set_allow_reader_connections(allow_reader_connections); + return *this; + } + + ConnectionStringBuilder& withMultiStatements(const bool& multi_statements) { + connection_string->set_multi_statements(multi_statements); + return *this; + } + + ConnectionStringBuilder& withEnableClusterFailover(const bool& enable_cluster_failover) { + connection_string->set_enable_cluster_failover(enable_cluster_failover); + return *this; + } + + ConnectionStringBuilder& withFailoverTimeout(const int& failover_t) { + connection_string->set_failover_timeout(failover_t); + return *this; + } + + ConnectionStringBuilder& withConnectTimeout(const int& connect_timeout) { + connection_string->set_connect_timeout(connect_timeout); + return *this; + } + + ConnectionStringBuilder& withNetworkTimeout(const int& network_timeout) { + connection_string->set_network_timeout(network_timeout); + return *this; + } + + ConnectionStringBuilder& withHostPattern(const std::string& host_pattern) { + connection_string->set_host_pattern(host_pattern); + return *this; + } + + ConnectionStringBuilder& withEnableFailureDetection(const bool& enable_failure_detection) { + connection_string->set_enable_failure_detection(enable_failure_detection); + return *this; + } + + ConnectionStringBuilder& withFailureDetectionTime(const int& failure_detection_time) { + connection_string->set_failure_detection_time(failure_detection_time); + return *this; + } + + ConnectionStringBuilder& withFailureDetectionTimeout(const int& failure_detection_timeout) { + connection_string->set_failure_detection_timeout(failure_detection_timeout); + return *this; + } + + ConnectionStringBuilder& withFailureDetectionInterval(const int& failure_detection_interval) { + connection_string->set_failure_detection_interval(failure_detection_interval); + return *this; + } + + ConnectionStringBuilder& withFailureDetectionCount(const int& failure_detection_count) { + connection_string->set_failure_detection_count(failure_detection_count); + return *this; + } + + ConnectionStringBuilder& withMonitorDisposalTime(const int& monitor_disposal_time) { + connection_string->set_monitor_disposal_time(monitor_disposal_time); + return *this; + } + + ConnectionStringBuilder& withReadTimeout(const int& read_timeout) { + connection_string->set_read_timeout(read_timeout); + return *this; + } + + ConnectionStringBuilder& withWriteTimeout(const int& write_timeout) { + connection_string->set_write_timeout(write_timeout); + return *this; + } + + std::string build() const { + if (connection_string->m_dsn.empty()) { + throw std::runtime_error("DSN is a required field in a connection string."); + } + if (connection_string->m_server.empty()) { + throw std::runtime_error("Server is a required field in a connection string."); + } + if (connection_string->m_port < 1) { + throw std::runtime_error("Port is a required field in a connection string."); + } + return connection_string->get_connection_string(); + } + + private: + std::unique_ptr connection_string; +}; diff --git a/integration/connection_string_builder_test.cc b/integration/connection_string_builder_test.cc new file mode 100644 index 00000000..2470e6d3 --- /dev/null +++ b/integration/connection_string_builder_test.cc @@ -0,0 +1,172 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include + +#include "connection_string_builder.cc" + +class ConnectionStringBuilderTest : public testing::Test { +}; + +// No fields are set in the builder. Error expected. +TEST_F(ConnectionStringBuilderTest, test_empty_builder) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + EXPECT_THROW(builder.build(), std::runtime_error); +} + +// More than one required field is not set in the builder. Error expected. +TEST_F(ConnectionStringBuilderTest, test_missing_fields) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + EXPECT_THROW(builder.withServer("testServer").build(), std::runtime_error); +} + +// Required field DSN is not set in the builder. Error expected. +TEST_F(ConnectionStringBuilderTest, test_missing_dsn) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + EXPECT_THROW(builder.withServer("testServer").withPort(3306).build(), std::runtime_error); +} + +// Required field Server is not set in the builder. Error expected. +TEST_F(ConnectionStringBuilderTest, test_missing_server) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + EXPECT_THROW(builder.withDSN("testDSN").withPort(3306).build(), std::runtime_error); +} + +// Required field Port is not set in the builder. Error expected. +TEST_F(ConnectionStringBuilderTest, test_missing_port) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + EXPECT_THROW(builder.withDSN("testDSN").withServer("testServer").build(), std::runtime_error); +} + +// All connection string fields are set in the builder. +TEST_F(ConnectionStringBuilderTest, test_complete_string) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + const std::string connection_string = builder.withServer("testServer") + .withUID("testUser") + .withPWD("testPwd") + .withLogQuery(false) + .withAllowReaderConnections(true) + .withMultiStatements(false) + .withDSN("testDSN") + .withFailoverTimeout(120000) + .withPort(3306) + .withDatabase("testDb") + .withConnectTimeout(20) + .withNetworkTimeout(20) + .withHostPattern("?.testDomain") + .withEnableFailureDetection(true) + .withFailureDetectionTime(10000) + .withFailureDetectionInterval(100) + .withFailureDetectionCount(4) + .withMonitorDisposalTime(300) + .withEnableClusterFailover(true) + .build(); + + const std::string expected = "DSN=testDSN;SERVER=testServer;PORT=3306;UID=testUser;PWD=testPwd;DATABASE=testDb;LOG_QUERY=0;ALLOW_READER_CONNECTIONS=1;MULTI_STATEMENTS=0;ENABLE_CLUSTER_FAILOVER=1;FAILOVER_TIMEOUT=120000;CONNECT_TIMEOUT=20;NETWORK_TIMEOUT=20;HOST_PATTERN=?.testDomain;ENABLE_FAILURE_DETECTION=1;FAILURE_DETECTION_TIME=10000;FAILURE_DETECTION_INTERVAL=100;FAILURE_DETECTION_COUNT=4;MONITOR_DISPOSAL_TIME=300;"; + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// No optional fields are set in the builder. Build will succeed. Connection string with required fields. +TEST_F(ConnectionStringBuilderTest, test_only_required_fields) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + const std::string connection_string = builder.withDSN("testDSN") + .withServer("testServer") + .withPort(3306) + .build(); + + const std::string expected = "DSN=testDSN;SERVER=testServer;PORT=3306;"; + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// Some optional fields are set and others not set in the builder. Build will succeed. +// Connection string with required fields and ONLY the fields that were set. +TEST_F(ConnectionStringBuilderTest, test_some_optional) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + const std::string connection_string = builder.withDSN("testDSN") + .withServer("testServer") + .withPort(3306) + .withUID("testUser") + .withPWD("testPwd") + .build(); + + const std::string expected("DSN=testDSN;SERVER=testServer;PORT=3306;UID=testUser;PWD=testPwd;"); + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// Boolean values are set in the builder. Build will succeed. True will be marked as 1 in the string, false 0. +TEST_F(ConnectionStringBuilderTest, test_setting_boolean_fields) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + const std::string connection_string = builder.withDSN("testDSN") + .withServer("testServer") + .withPort(3306) + .withUID("testUser") + .withPWD("testPwd") + .withLogQuery(false) + .withAllowReaderConnections(true) + .withMultiStatements(true) + .withEnableClusterFailover(false) + .withEnableFailureDetection(true) + .build(); + + const std::string expected("DSN=testDSN;SERVER=testServer;PORT=3306;UID=testUser;PWD=testPwd;LOG_QUERY=0;ALLOW_READER_CONNECTIONS=1;MULTI_STATEMENTS=1;ENABLE_CLUSTER_FAILOVER=0;ENABLE_FAILURE_DETECTION=1;"); + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// Create a builder with required values. Then append other properties to the builder. Then build the connection string. Build will succeed. +TEST_F(ConnectionStringBuilderTest, test_setting_multiple_steps_1) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + builder.withDSN("testDSN").withServer("testServer").withPort(3306); + + builder.withUID("testUser").withPWD("testPwd").withLogQuery(true); + const std::string connection_string = builder.build(); + + const std::string expected("DSN=testDSN;SERVER=testServer;PORT=3306;UID=testUser;PWD=testPwd;LOG_QUERY=1;"); + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// Create a builder initially without all required values. Then append other properties to the builder. +// After second round of values, all required values are set. Build the connection string. Build will succeed. +TEST_F(ConnectionStringBuilderTest, test_setting_multiple_steps_2) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + builder.withDSN("testDSN").withServer("testServer").withUID("testUser").withPWD("testPwd"); + + builder.withPort(3306).withLogQuery(true); + const std::string connection_string = builder.build(); + + const std::string expected("DSN=testDSN;SERVER=testServer;PORT=3306;UID=testUser;PWD=testPwd;LOG_QUERY=1;"); + EXPECT_EQ(0, expected.compare(connection_string)); +} + +// Create a builder with some values. Then append more values to the builder, but leaving required values unset. +// Then build the connection string. Error expected. +TEST_F(ConnectionStringBuilderTest, test_multiple_steps_without_required) { + ConnectionStringBuilder builder = ConnectionStringBuilder(); + builder.withServer("testServer").withPort(3306); + EXPECT_THROW(builder.withUID("testUser").withPWD("testPwd").withLogQuery(true).build(), std::runtime_error); +} diff --git a/integration/failover_integration_test.cc b/integration/failover_integration_test.cc new file mode 100644 index 00000000..0e142338 --- /dev/null +++ b/integration/failover_integration_test.cc @@ -0,0 +1,413 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "base_failover_integration_test.cc" + +class FailoverIntegrationTest : public BaseFailoverIntegrationTest { +protected: + std::string ACCESS_KEY = std::getenv("AWS_ACCESS_KEY_ID"); + std::string SECRET_ACCESS_KEY = std::getenv("AWS_SECRET_ACCESS_KEY"); + std::string SESSION_TOKEN = std::getenv("AWS_SESSION_TOKEN"); + Aws::Auth::AWSCredentials credentials = Aws::Auth::AWSCredentials(Aws::String(ACCESS_KEY), + Aws::String(SECRET_ACCESS_KEY), + Aws::String(SESSION_TOKEN)); + Aws::Client::ClientConfiguration client_config; + Aws::RDS::RDSClient rds_client; + SQLHENV env = nullptr; + SQLHDBC dbc = nullptr; + + static void SetUpTestSuite() { + Aws::InitAPI(options); + } + + static void TearDownTestSuite() { + Aws::ShutdownAPI(options); + } + + void SetUp() override { + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + client_config.region = "us-east-2"; + rds_client = Aws::RDS::RDSClient(credentials, client_config); + + cluster_instances = retrieve_topology_via_SDK(rds_client, cluster_id); + writer_id = get_writer_id(cluster_instances); + writer_endpoint = get_endpoint(writer_id); + readers = get_readers(cluster_instances); + reader_id = get_first_reader_id(cluster_instances); + reader_endpoint = get_proxied_endpoint(reader_id); + + builder = ConnectionStringBuilder(); + builder.withPort(MYSQL_PORT) + .withLogQuery(true) + .withEnableFailureDetection(true); + } + + void TearDown() override { + if (nullptr != dbc) { + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + if (nullptr != env) { + SQLFreeHandle(SQL_HANDLE_ENV, env); + } + } +}; + +/** +* Current writer dies, a reader instance is nominated to be a new writer, failover to the new +* writer. Driver failover occurs when executing a method against the connection +*/ +TEST_F(FailoverIntegrationTest, test_failFromWriterToNewWriter_failOnConnectionInvocation) { + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0"; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string current_connection_id = query_instance_id(dbc); + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_NE(current_connection_id, writer_id); + + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(FailoverIntegrationTest, test_takeOverConnectionProperties) { + SQLCHAR conn_out[4096] = "\0"; + SQLSMALLINT len; + + // Establish the topology cache so that we can later assert that new connections does not inherit properties from + // cached connection either before or after failover + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withMultiStatements(0).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); + + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withMultiStatements(1).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + SQLHSTMT handle; + const auto query = AS_SQLCHAR("select @@aurora_server_id; select 1; select 2;"); + + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + + // Verify that connection accepts multi-statement sql + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, query, SQL_NTS)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + // Verify that connection still accepts multi-statement SQL + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, query, SQL_NTS)); + + EXPECT_EQ(SQL_SUCCESS, SQLFreeHandle(SQL_HANDLE_STMT, handle)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/** Writer fails within a transaction. Open transaction with "SET autocommit = 0" */ +TEST_F(FailoverIntegrationTest, test_writerFailWithinTransaction_setAutocommitSqlZero) { + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0", message[SQL_MAX_MESSAGE_LENGTH] = "\0"; + SQLINTEGER native_error; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + // Setup tests + SQLHSTMT handle; + SQLSMALLINT stmt_length; + SQLCHAR sqlstate[6] = "\0"; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + const auto drop_table_query = AS_SQLCHAR("DROP TABLE IF EXISTS test3_1"); // Setting up tables + const auto create_table_query = AS_SQLCHAR("CREATE TABLE test3_1 (id INT NOT NULL PRIMARY KEY, test3_1_field VARCHAR(255) NOT NULL)"); + const auto setup_autocommit_query = AS_SQLCHAR("SET autocommit = 0"); // Open a new transaction + + // Execute setup query + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, create_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, setup_autocommit_query, SQL_NTS)); + + // Execute queries within the transaction + const auto insert_query_a = AS_SQLCHAR("INSERT INTO test3_1 VALUES (1, 'test field string 1')"); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, insert_query_a, SQL_NTS)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + + // If there is an active transaction (The insert queries), roll it back and return an error 08007. + EXPECT_EQ(SQL_ERROR, SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT)); + + // Check state + EXPECT_EQ(SQL_SUCCESS, SQLError(env, dbc, nullptr, sqlstate, &native_error, message, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length)); + const std::string state = reinterpret_cast(sqlstate); + EXPECT_EQ(ERROR_CONN_FAILURE_DURING_TX, state); + + // Query new ID after failover + std::string current_connection_id = query_instance_id(dbc); + + // Check if current connection is a new writer + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_NE(current_connection_id, writer_id); + + // No rows should have been inserted to the table + EXPECT_EQ(0, query_count_table_rows(handle, "test3_1")); + + // Clean up test + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/** Writer fails within a transaction. Open transaction with SQLSetConnectAttr */ +TEST_F(FailoverIntegrationTest, test_writerFailWithinTransaction_setAutoCommitFalse) { + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0", message[SQL_MAX_MESSAGE_LENGTH] = "\0"; + SQLINTEGER native_error; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + // Setup tests + SQLHSTMT handle; + SQLSMALLINT stmt_length; + SQLCHAR sqlstate[6] = "\0"; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + + const auto drop_table_query = AS_SQLCHAR("DROP TABLE IF EXISTS test3_2"); // Setting up tables + const auto create_table_query = AS_SQLCHAR("CREATE TABLE test3_2 (id INT NOT NULL PRIMARY KEY, test3_2_field VARCHAR(255) NOT NULL)"); + + // Execute setup query + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, create_table_query, SQL_NTS)); + + // Set autocommit = false + EXPECT_EQ(SQL_SUCCESS, SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, 0)); + + // Execute queries within the transaction + const auto insert_query_a = AS_SQLCHAR("INSERT INTO test3_2 VALUES (1, 'test field string 1')"); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, insert_query_a, SQL_NTS)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + + // If there is an active transaction, roll it back and return an error 08007. + EXPECT_EQ(SQL_ERROR, SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT)); + + // Check state + EXPECT_EQ(SQL_SUCCESS, SQLError(env, dbc, nullptr, sqlstate, &native_error, message, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length)); + const std::string state = reinterpret_cast(sqlstate); + EXPECT_EQ(ERROR_CONN_FAILURE_DURING_TX, state); + + // Query new ID after failover + std::string current_connection_id = query_instance_id(dbc); + + // Check if current connection is a new writer + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_NE(current_connection_id, writer_id); + + // No rows should have been inserted to the table + EXPECT_EQ(0, query_count_table_rows(handle, "test3_2")); + + // Clean up test + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/** Writer fails within a transaction. Open transaction with "START TRANSACTION". */ +TEST_F(FailoverIntegrationTest, test_writerFailWithinTransaction_startTransaction) { + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0", message[SQL_MAX_MESSAGE_LENGTH] = "\0"; + SQLINTEGER native_error; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + // Setup tests + SQLHSTMT handle; + SQLSMALLINT stmt_length; + SQLCHAR sqlstate[6] = "\0"; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + const auto drop_table_query = AS_SQLCHAR("DROP TABLE IF EXISTS test3_3"); // Setting up tables + const auto create_table_query = AS_SQLCHAR("CREATE TABLE test3_3 (id INT NOT NULL PRIMARY KEY, test3_3_field VARCHAR(255) NOT NULL)"); + const auto start_trans_query = AS_SQLCHAR("START TRANSACTION"); // Open a new transaction + + // Execute setup query + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, create_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, start_trans_query, SQL_NTS)); + + // Execute queries within the transaction + const auto insert_query_a = AS_SQLCHAR("INSERT INTO test3_3 VALUES (1, 'test field string 1')"); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, insert_query_a, SQL_NTS)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + + // If there is an active transaction (The insert queries), roll it back and return an error 08007. + EXPECT_EQ(SQL_ERROR, SQLEndTran(SQL_HANDLE_DBC, dbc, SQL_COMMIT)); + + // Check state + EXPECT_EQ(SQL_SUCCESS, SQLError(env, dbc, nullptr, sqlstate, &native_error, message, SQL_MAX_MESSAGE_LENGTH - 1, &stmt_length)); + const std::string state = reinterpret_cast(sqlstate); + EXPECT_EQ(ERROR_CONN_FAILURE_DURING_TX, state); + + // Query new ID after failover + std::string current_connection_id = query_instance_id(dbc); + + // Check if current connection is a new writer + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_NE(current_connection_id, writer_id); + + // No rows should have been inserted to the table + EXPECT_EQ(0, query_count_table_rows(handle, "test3_3")); + + // Clean up test + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/* Writer fails within NO transaction. */ +TEST_F(FailoverIntegrationTest, test_writerFailWithNoTransaction) { + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0"; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + // Setup tests + SQLHSTMT handle; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + const auto drop_table_query = AS_SQLCHAR("DROP TABLE IF EXISTS test3_4"); // Setting up tables + const auto setup_table_query = AS_SQLCHAR("CREATE TABLE test3_4 (id int not null primary key, test3_2_field varchar(255) not null)"); + + // Execute setup query + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, setup_table_query, SQL_NTS)); + + // Have something inserted into table + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + const auto insert_query_a = AS_SQLCHAR("INSERT INTO test3_4 VALUES (1, 'test field string 1')"); + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, insert_query_a, SQL_NTS)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id); + + // Query expected to fail and rollback things in transaction + const auto insert_query_b = AS_SQLCHAR("INSERT INTO test3_4 VALUES (2, 'test field string 2')"); + + // Execute query expecting failure & rollback insert 2 + assert_query_failed(dbc, insert_query_b, ERROR_COMM_LINK_CHANGED); + + // Query new ID after failover + const std::string current_connection_id = query_instance_id(dbc); + + // Check if current connection is a new writer + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_NE(current_connection_id, writer_id); + + // ID 1 should have 1 row, ID 2 should have NO rows + EXPECT_EQ(1, query_count_table_rows(handle, "test3_4", 1)); + EXPECT_EQ(0, query_count_table_rows(handle, "test3_4", 2)); + + // Clean up test + EXPECT_EQ(SQL_SUCCESS, SQLExecDirect(handle, drop_table_query, SQL_NTS)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/** + * Current reader dies, no other reader instance, failover to writer, then writer dies, failover + * to another available reader instance. + */ +TEST_F(FailoverIntegrationTest, test_failFromReaderToWriterToAnyAvailableInstance) { + // Ensure all networks to instances are enabled + for (const auto& x : proxy_map) { + enable_connectivity(x.second); + } + + // Disable all readers but one & writer + for (size_t index = 1; index < readers.size(); ++index) { + disable_instance(readers[index]); + } + + const std::string initial_writer_id = writer_id; + const std::string initial_reader_id = reader_id; + const std::string initial_reader_endpoint = get_proxied_endpoint(initial_reader_id); + + SQLCHAR conn_out[4096]; + SQLSMALLINT len; + + ConnectionStringBuilder proxied_builder = ConnectionStringBuilder(); + proxied_builder.withDSN(dsn).withUID(user).withPWD(pwd).withConnectTimeout(10).withNetworkTimeout(10); + proxied_builder.withPort(MYSQL_PROXY_PORT).withHostPattern(PROXIED_CLUSTER_TEMPLATE).withLogQuery(true); + connection_string = proxied_builder.withServer(initial_reader_endpoint).withAllowReaderConnections(true).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + disable_instance(initial_reader_id); + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + std::string current_connection = query_instance_id(dbc); + EXPECT_EQ(current_connection, initial_writer_id); + + // Re-enable 2 readers (Second & Third reader) + const std::string second_reader_id = readers[1]; + const std::string third_reader_id = readers[2]; + enable_instance(second_reader_id); + enable_instance(third_reader_id); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, initial_writer_id); + + // Query to trigger failover (Initial Writer) + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + // Expect that we're connected to reader 2 or 3 + current_connection = query_instance_id(dbc); + EXPECT_TRUE(current_connection == second_reader_id || current_connection == third_reader_id); + + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +/* Pooled connection tests. */ + +/* Writer connection failover within the connection pool. */ +TEST_F(FailoverIntegrationTest, test_pooledWriterConnection_BasicFailover) { + const std::string nominated_writer_id = cluster_instances[1]; + + // Enable connection pooling + EXPECT_EQ(SQL_SUCCESS, SQLSetEnvAttr(NULL, SQL_ATTR_CONNECTION_POOLING, reinterpret_cast(SQL_CP_ONE_PER_DRIVER), 0)); + EXPECT_EQ(SQL_SUCCESS, SQLSetEnvAttr(env, SQL_ATTR_CP_MATCH, SQL_CP_STRICT_MATCH, 0)); + + connection_string = builder.withDSN(dsn).withServer(writer_endpoint).withUID(user).withPWD(pwd).withDatabase(db).build(); + SQLCHAR conn_out[4096] = "\0"; + SQLSMALLINT len; + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + failover_cluster_and_wait_until_writer_changed(rds_client, cluster_id, writer_id, nominated_writer_id); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string current_connection_id = query_instance_id(dbc); + const std::string next_writer_id = get_DB_cluster_writer_instance_id(rds_client, cluster_id); + EXPECT_TRUE(is_DB_instance_writer(rds_client, cluster_id, current_connection_id)); + EXPECT_EQ(next_writer_id, current_connection_id); + EXPECT_EQ(nominated_writer_id, current_connection_id); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} diff --git a/integration/failover_performance_test.cc b/integration/failover_performance_test.cc new file mode 100644 index 00000000..31c74a03 --- /dev/null +++ b/integration/failover_performance_test.cc @@ -0,0 +1,449 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "base_failover_integration_test.cc" + +#include +#include +#include +#include +#include +#include + +#include + +#define SOCKET_TIMEOUT_TEST_ID 1 +#define EFM_FAILOVER_TEST_ID 2 +#define EFM_DETECTION_TEST_ID 3 +#define DEFAULT_FAILURE_DETECTION_TIMEOUT 5 + +struct BASE_PERFORMANCE_DATA { + int network_outage_delay; + int min_failover_time, max_failover_time, avg_failover_time; + virtual void write_header(std::ofstream& data_stream) {} + virtual void write_data_row(std::ofstream& data_stream) {} + virtual void write_header_xlsx(OpenXLSX::XLWorksheet worksheet, int row) {} + virtual void write_data_row_xlsx(OpenXLSX::XLWorksheet worksheet, int row) {} +}; + +struct SOCKET_PERFORMANCE_DATA : BASE_PERFORMANCE_DATA { + int failover_timeout, connect_timeout, network_timeout; + + void write_header(std::ofstream& data_stream) override { + data_stream << "Outage Delay (ms), Failover Timeout (ms), Connect Timeout (s), Network Timeout (s), Min Failover Time (ms), Max Failover Time (ms), Avg Failover Time (ms)\n"; + } + + void write_header_xlsx(const OpenXLSX::XLWorksheet worksheet, int row) { + worksheet.cell(row, 1).value() = "Outage Delay (ms)"; + worksheet.cell(row, 2).value() = "Failover Timeout (ms)"; + worksheet.cell(row, 3).value() = "Connect Timeout (s)"; + worksheet.cell(row, 4).value() = "Network Timeout (s)"; + worksheet.cell(row, 5).value() = "Min Failover Time (ms)"; + worksheet.cell(row, 6).value() = "Max Failover Time (ms)"; + worksheet.cell(row, 7).value() = "Avg Failover Time (ms)"; + } + + void write_data_row(std::ofstream& data_stream) override { + char data_row[4096]; + sprintf(data_row, "%d, %d, %d, %d, %d, %d, %d\n", + this->network_outage_delay, + this->failover_timeout, + this->connect_timeout, + this->network_timeout, + this->min_failover_time, + this->max_failover_time, + this->avg_failover_time + ); + data_stream << data_row; + } + + void write_data_row_xlsx(const OpenXLSX::XLWorksheet worksheet, int row) override { + worksheet.cell(row, 1).value() = this->network_outage_delay; + worksheet.cell(row, 2).value() = this->failover_timeout; + worksheet.cell(row, 3).value() = this->connect_timeout; + worksheet.cell(row, 4).value() = this->network_timeout; + worksheet.cell(row, 5).value() = this->min_failover_time; + worksheet.cell(row, 6).value() = this->max_failover_time; + worksheet.cell(row, 7).value() = this->avg_failover_time; + } +}; + +struct EFM_PERFORMANCE_DATA : BASE_PERFORMANCE_DATA { + int detection_time, detection_interval, detection_count; + + void write_header(std::ofstream& data_stream) override { + data_stream << "Outage Delay (ms), Detection Time (ms), Detection Interval (ms), Detection Count, Min Failover Time (ms), Max Failover Time (ms), Avg Failover Time (ms)\n"; + } + + void write_header_xlsx(const OpenXLSX::XLWorksheet worksheet, int row) { + worksheet.cell(row, 1).value() = "Outage Delay (ms)"; + worksheet.cell(row, 2).value() = "Detection Time (ms)"; + worksheet.cell(row, 3).value() = "Detection Interval (ms)"; + worksheet.cell(row, 4).value() = "Detection Count"; + worksheet.cell(row, 5).value() = "Min Failover Time (ms)"; + worksheet.cell(row, 6).value() = "Max Failover Time (ms)"; + worksheet.cell(row, 7).value() = "Avg Failover Time (ms)"; + } + + void write_data_row(std::ofstream& data_stream) override { + char data_row[4096]; + sprintf(data_row, "%d, %d, %d, %d, %d, %d, %d\n", + this->network_outage_delay, + this->detection_time, + this->detection_interval, + this->detection_count, + this->min_failover_time, + this->max_failover_time, + this->avg_failover_time + ); + data_stream << data_row; + } + + void write_data_row_xlsx(const OpenXLSX::XLWorksheet worksheet, int row) override { + worksheet.cell(row, 1).value() = this->network_outage_delay; + worksheet.cell(row, 2).value() = this->detection_time; + worksheet.cell(row, 3).value() = this->detection_interval; + worksheet.cell(row, 4).value() = this->detection_count; + worksheet.cell(row, 5).value() = this->min_failover_time; + worksheet.cell(row, 6).value() = this->max_failover_time; + worksheet.cell(row, 7).value() = this->avg_failover_time; + } +}; + +static std::vector socket_failover_data; +static std::vector efm_failover_data; +static std::vector efm_detection_data; + +class FailoverPerformanceTest : + public ::testing::WithParamInterface>, + public BaseFailoverIntegrationTest { +protected: + std::string ACCESS_KEY = std::getenv("AWS_ACCESS_KEY_ID"); + std::string SECRET_ACCESS_KEY = std::getenv("AWS_SECRET_ACCESS_KEY"); + std::string SESSION_TOKEN = std::getenv("AWS_SESSION_TOKEN"); + Aws::Auth::AWSCredentials credentials = Aws::Auth::AWSCredentials(Aws::String(ACCESS_KEY), + Aws::String(SECRET_ACCESS_KEY), + Aws::String(SESSION_TOKEN)); + Aws::Client::ClientConfiguration client_config; + Aws::RDS::RDSClient rds_client; + SQLHENV env = nullptr; + SQLHDBC dbc = nullptr; + + SQLCHAR* LONG_QUERY = AS_SQLCHAR("SELECT SLEEP(600)"); // 600s -> 10m + const size_t NB_OF_RUNS = 6; + static constexpr char* OUTPUT_FILE_PATH = "./build/reports/"; + + static void SetUpTestSuite() { + Aws::InitAPI(options); + } + + static void TearDownTestSuite() { + Aws::ShutdownAPI(options); + + // Save results to spreadsheet + write_metrics_to_xlsx("failover_performance.xlsx", socket_failover_data); + + // Save results from EFM performance tests + write_metrics_to_xlsx("efm_performance.xlsx", efm_failover_data); + + // Save results from EFM without performance tests + write_metrics_to_xlsx("efm_detection_performance.xlsx", efm_detection_data); + } + + void SetUp() override { + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + client_config.region = "us-east-2"; + rds_client = Aws::RDS::RDSClient(credentials, client_config); + + cluster_instances = retrieve_topology_via_SDK(rds_client, cluster_id); + writer_id = get_writer_id(cluster_instances); + } + + void TearDown() override { + if (nullptr != dbc) { + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + if (nullptr != env) { + SQLFreeHandle(SQL_HANDLE_ENV, env); + } + } + + // Only run if passed in parameter: data, is a type of BASE_PERFORMANCE_DATA + template::value>> + bool measure_performance(const std::string& conn_str, const int sleep_delay, DERIVED_PERFORMANCE_DATA& data) { + std::atomic downtime(std::chrono::steady_clock::now()); + std::vector recorded_metrics; + + for (size_t i = 0; i < NB_OF_RUNS; i++) { + // Ensure all proxies are up + for (const auto& x : proxy_map) { + enable_connectivity(x.second); + } + + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(conn_str.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + SQLHSTMT handle; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + + // Start thread for shutdown + auto network_shutdown_function = [&](const std::string& instance_id, const int sleep_ms, std::atomic& downtime_detection) { + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + disable_instance(instance_id); + downtime_detection.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); + }; + std::thread network_shutdown_thread(network_shutdown_function, std::cref(writer_id), std::cref(sleep_delay), std::ref(downtime)); + + // Execute long query and wait for error / failover. + if (SQL_ERROR == SQLExecDirect(handle, LONG_QUERY, SQL_NTS)) { + int failover_time = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - downtime.load(std::memory_order_relaxed)).count()); + recorded_metrics.push_back(failover_time); + } + + // Ensure thread stops + if (network_shutdown_thread.joinable()) { + network_shutdown_thread.join(); + } + + EXPECT_EQ(SQL_SUCCESS, SQLFreeHandle(SQL_HANDLE_STMT, handle)); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); + } + + // Metrics Statistics + const size_t recorded_metrics_size = recorded_metrics.size(); + if (recorded_metrics_size < 1) { + return false; + } + int min = recorded_metrics[0]; + int max = recorded_metrics[0]; + int sum = recorded_metrics[0]; + for (size_t i = 1; i < recorded_metrics_size; i++) { + min = std::min(min, recorded_metrics[i]); + max = std::max(max, recorded_metrics[i]); + sum += recorded_metrics[i]; + } + int avg = static_cast(sum / recorded_metrics_size); + + data.min_failover_time = min; + data.max_failover_time = max; + data.avg_failover_time = avg; + return true; + } + + // Only run if passed in parameter: data_list contents, are a type of BASE_PERFORMANCE_DATA + template::value>> + static void write_metrics_to_file(const char* file_name, const std::vector& data_list) { + if (data_list.size() < 2) { + return; + } + + std::ofstream data_file(std::string(OUTPUT_FILE_PATH) + file_name); + + DERIVED_PERFORMANCE_DATA data = data_list[0]; + data.write_header(data_file); + for (size_t i = 1; i < data_list.size(); i++) { + data = data_list[i]; + data.write_data_row(data_file); + } + + data_file.close(); + } + + template::value>> + static void write_metrics_to_xlsx(const char* file_name, const std::vector& data_list) { + if (data_list.size() < 2) { + return; + } + + OpenXLSX::XLDocument doc; + doc.create(file_name); + + std::string XLSX_WORKSHEET_NAME = "failover_performance"; + doc.workbook().addWorksheet(XLSX_WORKSHEET_NAME); + OpenXLSX::XLWorksheet worksheet = doc.workbook().worksheet(XLSX_WORKSHEET_NAME); + + DERIVED_PERFORMANCE_DATA data = data_list[0]; + + int row = 1; + data.write_header_xlsx(worksheet, row); + + for (size_t i = 1; i < data_list.size(); i++) { + data = data_list[i]; + row++; + data.write_data_row_xlsx(worksheet, row); + } + + doc.save(); + doc.close(); + } +}; + +TEST_P(FailoverPerformanceTest, test_measure_failover) { + const int test_type = std::get<0>(GetParam()); + const int sleep_delay = std::get<1>(GetParam()); + const std::string server = get_proxied_endpoint(writer_id); + + builder.withDSN(dsn).withServer(server).withPort(MYSQL_PROXY_PORT) + .withDatabase(db).withUID(user).withPWD(pwd).withHostPattern(PROXIED_CLUSTER_TEMPLATE) + .withAllowReaderConnections(true); + + switch (test_type) { + case SOCKET_TIMEOUT_TEST_ID: { + const int failover_timeout = std::get<2>(GetParam()); + const int connect_timeout = std::get<3>(GetParam()); + const int network_timeout = std::get<4>(GetParam()); + + const std::string conn_str = builder.withEnableFailureDetection(false) + .withEnableClusterFailover(true) + .withFailoverTimeout(failover_timeout) + .withConnectTimeout(connect_timeout) + .withNetworkTimeout(network_timeout) + .withLogQuery(true).build(); + + SOCKET_PERFORMANCE_DATA data; + data.network_outage_delay = sleep_delay; + data.failover_timeout = failover_timeout; + data.connect_timeout = connect_timeout; + data.network_timeout = network_timeout; + + if (measure_performance(conn_str, sleep_delay, data)) { + socket_failover_data.push_back(data); + } + break; + } + + case EFM_FAILOVER_TEST_ID: { + const int detection_time = std::get<2>(GetParam()); + const int detection_interval = std::get<3>(GetParam()); + const int detection_count = std::get<4>(GetParam()); + + const std::string conn_str = builder.withEnableFailureDetection(true) + .withFailureDetectionTime(detection_time) + .withFailureDetectionInterval(detection_interval) + .withFailureDetectionCount(detection_count) + .withFailureDetectionTimeout(DEFAULT_FAILURE_DETECTION_TIMEOUT) + .withEnableClusterFailover(true) + .withFailoverTimeout(60000) + .withConnectTimeout(120) + .withNetworkTimeout(120) + .withLogQuery(true).build(); + + EFM_PERFORMANCE_DATA data; + data.network_outage_delay = sleep_delay; + data.detection_time = detection_time; + data.detection_interval = detection_interval; + data.detection_count = detection_count; + + if (measure_performance(conn_str, sleep_delay, data)) { + efm_failover_data.push_back(data); + } + break; + } + + case EFM_DETECTION_TEST_ID: { + const int detection_time = std::get<2>(GetParam()); + const int detection_interval = std::get<3>(GetParam()); + const int detection_count = std::get<4>(GetParam()); + + const std::string conn_str = builder.withEnableFailureDetection(true) + .withFailureDetectionTime(detection_time) + .withFailureDetectionInterval(detection_interval) + .withFailureDetectionCount(detection_count) + .withFailureDetectionTimeout(DEFAULT_FAILURE_DETECTION_TIMEOUT) + .withEnableClusterFailover(false) + .withReadTimeout(120) + .withWriteTimeout(120) + .withLogQuery(true).build(); + + EFM_PERFORMANCE_DATA data; + data.network_outage_delay = sleep_delay; + data.detection_time = detection_time; + data.detection_interval = detection_interval; + data.detection_count = detection_count; + + if (measure_performance(conn_str, sleep_delay, data)) { + efm_detection_data.push_back(data); + } + break; + } + + default: + throw std::runtime_error("No Such Parameterized Test Case"); + } +} + +INSTANTIATE_TEST_CASE_P( + SocketTimeoutTest, + FailoverPerformanceTest, + // Test Type, Sleep_Delay_ms, Failover_Timeout_ms, Connection_Timeout_s, Network_Timeout_s + ::testing::Values( + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 5000, 30000, 30, 30), + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 10000, 30000, 30, 30), + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 15000, 30000, 30, 30), + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 20000, 30000, 30, 30), + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 25000, 30000, 30, 30), + std::make_tuple(SOCKET_TIMEOUT_TEST_ID, 30000, 30000, 30, 30) + ) +); + +INSTANTIATE_TEST_CASE_P( + EFMTimeoutTest, + FailoverPerformanceTest, + // Test Type, Sleep Delay, detection grace time, detection interval, detection count + ::testing::Values( + std::make_tuple(EFM_FAILOVER_TEST_ID, 5000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 10000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 15000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 20000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 25000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 30000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 35000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 40000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 45000, 30000, 5000, 3), + std::make_tuple(EFM_FAILOVER_TEST_ID, 50000, 30000, 5000, 3) + ) +); + +INSTANTIATE_TEST_CASE_P( + EFMDetectionTimeoutTest, + FailoverPerformanceTest, + // Test Type, Sleep Delay, detection grace time, detection interval, detection count + ::testing::Values( + std::make_tuple(EFM_DETECTION_TEST_ID, 5000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 10000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 15000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 20000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 25000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 30000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 35000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 40000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 45000, 30000, 5000, 3), + std::make_tuple(EFM_DETECTION_TEST_ID, 50000, 30000, 5000, 3) + ) +); diff --git a/integration/network_failover_integration_test.cc b/integration/network_failover_integration_test.cc new file mode 100644 index 00000000..27061ca0 --- /dev/null +++ b/integration/network_failover_integration_test.cc @@ -0,0 +1,308 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License, version 2.0 +// (GPLv2), as published by the Free Software Foundation, with the +// following additional permissions: +// +// This program is distributed with certain software that is licensed +// under separate terms, as designated in a particular file or component +// or in the license documentation. Without limiting your rights under +// the GPLv2, the authors of this program hereby grant you an additional +// permission to link the program and your derivative works with the +// separately licensed software that they have included with the program. +// +// Without limiting the foregoing grant of rights under the GPLv2 and +// additional permission as to separately licensed software, this +// program is also subject to the Universal FOSS Exception, version 1.0, +// a copy of which can be found along with its FAQ at +// http://oss.oracle.com/licenses/universal-foss-exception. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License, version 2.0, for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// http://www.gnu.org/licenses/gpl-2.0.html. + +#include "base_failover_integration_test.cc" + +class NetworkFailoverIntegrationTest : public BaseFailoverIntegrationTest { +protected: + std::string ACCESS_KEY = std::getenv("AWS_ACCESS_KEY_ID"); + std::string SECRET_ACCESS_KEY = std::getenv("AWS_SECRET_ACCESS_KEY"); + std::string SESSION_TOKEN = std::getenv("AWS_SESSION_TOKEN"); + Aws::Auth::AWSCredentials credentials = Aws::Auth::AWSCredentials(Aws::String(ACCESS_KEY), + Aws::String(SECRET_ACCESS_KEY), + Aws::String(SESSION_TOKEN)); + Aws::Client::ClientConfiguration client_config; + Aws::RDS::RDSClient rds_client; + SQLHENV env = nullptr; + SQLHDBC dbc = nullptr; + + static void SetUpTestSuite() { + Aws::InitAPI(options); + } + + static void TearDownTestSuite() { + Aws::ShutdownAPI(options); + } + + void SetUp() override { + SQLAllocHandle(SQL_HANDLE_ENV, nullptr, &env); + SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, reinterpret_cast(SQL_OV_ODBC3), 0); + SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); + + client_config.region = "us-east-2"; + rds_client = Aws::RDS::RDSClient(credentials, client_config); + + for (const auto& x : proxy_map) { + enable_connectivity(x.second); + } + + cluster_instances = retrieve_topology_via_SDK(rds_client, cluster_id); + writer_id = get_writer_id(cluster_instances); + writer_endpoint = get_proxied_endpoint(writer_id); + readers = get_readers(cluster_instances); + reader_id = get_first_reader_id(cluster_instances); + reader_endpoint = get_proxied_endpoint(reader_id); + + builder = ConnectionStringBuilder(); + builder.withDSN(dsn) + .withUID(user) + .withPWD(pwd) + .withConnectTimeout(10) + .withNetworkTimeout(10); + builder.withPort(MYSQL_PROXY_PORT) + .withHostPattern(PROXIED_CLUSTER_TEMPLATE) + .withLogQuery(true) + .withEnableFailureDetection(true); + } + + void TearDown() override { + if (nullptr != dbc) { + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + } + if (nullptr != env) { + SQLFreeHandle(SQL_HANDLE_ENV, env); + } + } +}; + +TEST_F(NetworkFailoverIntegrationTest, connection_test) { + test_connection(dbc, MYSQL_INSTANCE_1_URL, MYSQL_PORT); + test_connection(dbc, MYSQL_INSTANCE_1_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT); + test_connection(dbc, MYSQL_CLUSTER_URL, MYSQL_PORT); + test_connection(dbc, MYSQL_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT); + test_connection(dbc, MYSQL_RO_CLUSTER_URL, MYSQL_PORT); + test_connection(dbc, MYSQL_RO_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT); +} + +TEST_F(NetworkFailoverIntegrationTest, lost_connection_to_writer) { + const std::string server = get_proxied_endpoint(writer_id); + connection_string = builder.withServer(server).withFailoverTimeout(GLOBAL_FAILOVER_TIMEOUT).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + assert_query_succeeded(dbc, SERVER_ID_QUERY); + + const auto writer_proxy = get_proxy_from_map(writer_id); + if (writer_proxy) { + disable_connectivity(writer_proxy); + } + disable_connectivity(proxy_cluster); + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_FAILURE); + + enable_connectivity(writer_proxy); + enable_connectivity(proxy_cluster); + + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, use_same_connection_after_failing_failover) { + const std::string server = get_proxied_endpoint(writer_id); + connection_string = builder.withServer(server).withFailoverTimeout(GLOBAL_FAILOVER_TIMEOUT).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + SQLHSTMT handle; + EXPECT_EQ(SQL_SUCCESS, SQLAllocHandle(SQL_HANDLE_STMT, dbc, &handle)); + + assert_query_succeeded(dbc, SERVER_ID_QUERY); + + const auto writer_proxy = get_proxy_from_map(writer_id); + if (writer_proxy) { + disable_connectivity(writer_proxy); + } + disable_connectivity(proxy_cluster); + + // failover fails + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_FAILURE); + + enable_connectivity(writer_proxy); + enable_connectivity(proxy_cluster); + + // Reuse same connection after failing failover + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_FAILURE); + + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, lost_connection_to_all_readers) { + connection_string = builder.withServer(reader_endpoint).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + for (const auto& x : proxy_map) { + if (x.first != writer_id) { + disable_connectivity(x.second); + } + } + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string new_reader_id = query_instance_id(dbc); + EXPECT_EQ(writer_id, new_reader_id); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, lost_connection_to_reader_instance) { + connection_string = builder.withServer(reader_endpoint).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + disable_instance(reader_id); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string new_instance = query_instance_id(dbc); + EXPECT_EQ(writer_id, new_instance); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, lost_connection_read_only) { + connection_string = builder.withServer(reader_endpoint).withAllowReaderConnections(true).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + disable_instance(reader_id); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string new_reader_id = query_instance_id(dbc); + EXPECT_NE(writer_id, new_reader_id); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, writer_connection_fails_due_to_no_reader) { + const char* writer_char_id = writer_id.c_str(); + const std::string server = MYSQL_INSTANCE_1_URL + PROXIED_DOMAIN_NAME_SUFFIX; + + connection_string = builder.withServer(server).withFailoverTimeout(GLOBAL_FAILOVER_TIMEOUT).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + // Put all but writer down first + for (const auto& x : proxy_map) { + if (x.first != writer_char_id) { + disable_connectivity(x.second); + } + } + + // Crash the writer now + const auto writer_proxy = get_proxy_from_map(writer_id); + if (writer_proxy) { + disable_connectivity(writer_proxy); + } + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_FAILURE); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, fail_from_reader_to_reader_with_some_readers_are_down) { + // Assert there are at least 2 readers in the cluster. + EXPECT_LE(2, readers.size()); + + connection_string = builder.withServer(reader_endpoint).withFailoverTimeout(GLOBAL_FAILOVER_TIMEOUT).withAllowReaderConnections(true).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + for (size_t index = 0; index < readers.size() - 1; ++index) { + disable_instance(readers[index]); + } + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string current_connection = query_instance_id(dbc); + const std::string last_reader = readers.back(); + + // Assert that new instance is either the last reader instance or the writer instance. + EXPECT_TRUE(current_connection == last_reader || current_connection == writer_id); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} + +TEST_F(NetworkFailoverIntegrationTest, failover_back_to_the_previously_down_reader) { + // Assert there are at least 4 readers in the cluster. + EXPECT_LE(4, readers.size()); + + std::vector previous_readers; + + const std::string first_reader = reader_id; + const std::string server = get_proxied_endpoint(first_reader); + previous_readers.push_back(first_reader); + + connection_string = builder.withServer(server).withFailoverTimeout(GLOBAL_FAILOVER_TIMEOUT).withAllowReaderConnections(true).build(); + EXPECT_EQ(SQL_SUCCESS, SQLDriverConnect(dbc, nullptr, AS_SQLCHAR(connection_string.c_str()), SQL_NTS, conn_out, MAX_NAME_LEN, &len, SQL_DRIVER_NOPROMPT)); + + disable_instance(first_reader); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string second_reader = query_instance_id(dbc); + EXPECT_TRUE(is_db_instance_reader(second_reader)); + assert_is_new_reader(previous_readers, second_reader); + previous_readers.push_back(second_reader); + + disable_instance(second_reader); + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string third_reader = query_instance_id(dbc); + EXPECT_TRUE(is_db_instance_reader(third_reader)); + assert_is_new_reader(previous_readers, third_reader); + previous_readers.push_back(third_reader); + + // Find the fourth reader instance + std::string last_reader; + for (const auto& reader : readers) { + const std::string reader_id = reader; + bool is_same = false; + + for (const auto& used_reader : previous_readers) { + if (used_reader == reader_id) { + is_same = true; + break; + } + } + + if (is_same) { + continue; + } + + last_reader = reader_id; + } + + assert_is_new_reader(previous_readers, last_reader); + + // Crash the fourth reader instance. + disable_instance(last_reader); + + // Stop crashing the first and second. + enable_instance(previous_readers[0]); + enable_instance(previous_readers[1]); + + const std::string current_instance_id = query_instance_id(dbc); + EXPECT_EQ(third_reader, current_instance_id); + + // Start crashing the third instance. + disable_instance(third_reader); + + assert_query_failed(dbc, SERVER_ID_QUERY, ERROR_COMM_LINK_CHANGED); + + const std::string last_instance_id = query_instance_id(dbc); + + // Assert that the last instance is either the first reader instance or the second reader instance. + EXPECT_TRUE(last_instance_id == first_reader || last_instance_id == second_reader); + EXPECT_EQ(SQL_SUCCESS, SQLDisconnect(dbc)); +} diff --git a/mysql_strings/conf_to_src.cc b/mysql_strings/conf_to_src.cc index 7d890603..2c072a11 100644 --- a/mysql_strings/conf_to_src.cc +++ b/mysql_strings/conf_to_src.cc @@ -279,14 +279,14 @@ int main(int argc, char **argv [[maybe_unused]]) { memset(&ncs, 0, sizeof(ncs)); memset(&all_charsets, 0, sizeof(all_charsets)); - sprintf(filename, "%s/%s", argv[1], "Index.xml"); + snprintf(filename, sizeof(filename), "%s/%s", argv[1], "Index.xml"); my_read_charset_file(filename); for (cs = all_charsets; cs < all_charsets + array_elements(all_charsets); cs++) { if (cs->number && !(cs->state & MY_CS_COMPILED)) { if ((!simple_cs_is_full(cs)) && (cs->csname)) { - sprintf(filename, "%s/%s.xml", argv[1], cs->csname); + snprintf(filename, sizeof(filename), "%s/%s.xml", argv[1], cs->csname); my_read_charset_file(filename); } } diff --git a/mysql_strings/ctype.cc b/mysql_strings/ctype.cc index 4f19aa83..11ee6d43 100644 --- a/mysql_strings/ctype.cc +++ b/mysql_strings/ctype.cc @@ -367,7 +367,7 @@ static int tailoring_append(MY_XML_PARSER *st, const char *fmt, size_t len, size_t newlen = i->tailoring_length + len + 64; /* 64 for format */ if (MY_XML_OK == my_charset_file_tailoring_realloc(i, newlen)) { char *dst = i->tailoring + i->tailoring_length; - sprintf(dst, fmt, (int)len, attr); + snprintf(dst, sizeof(dst), fmt, (int)len, attr); i->tailoring_length += strlen(dst); return MY_XML_OK; } @@ -381,7 +381,7 @@ static int tailoring_append2(MY_XML_PARSER *st, const char *fmt, size_t len1, size_t newlen = i->tailoring_length + len1 + len2 + 64; /* 64 for format */ if (MY_XML_OK == my_charset_file_tailoring_realloc(i, newlen)) { char *dst = i->tailoring + i->tailoring_length; - sprintf(dst, fmt, (int)len1, attr1, (int)len2, attr2); + snprintf(dst, sizeof(dst), fmt, (int)len1, attr1, (int)len2, attr2); i->tailoring_length += strlen(dst); return MY_XML_OK; } @@ -754,9 +754,9 @@ bool my_parse_charset_xml(MY_CHARSET_LOADER *loader, const char *buf, if (rc != MY_XML_OK) { const char *errstr = my_xml_error_string(&p); if (sizeof(loader->errarg) > 32 + strlen(errstr)) { - sprintf(loader->errarg, "at line %d pos %d: %s", - my_xml_error_lineno(&p) + 1, (int)my_xml_error_pos(&p), - my_xml_error_string(&p)); + snprintf(loader->errarg, sizeof(loader->errarg), "at line %d pos %d: %s", + my_xml_error_lineno(&p) + 1, (int)my_xml_error_pos(&p), + my_xml_error_string(&p)); } } return rc; diff --git a/mysql_strings/uca-dump.cc b/mysql_strings/uca-dump.cc index 902cdcf6..343ad454 100644 --- a/mysql_strings/uca-dump.cc +++ b/mysql_strings/uca-dump.cc @@ -278,7 +278,7 @@ static const char *lname[] = {"primary", "secondary", "tertiary", "quaternary"}; static char *prefix_name(MY_UCA *uca) { static char prefix[MY_UCA_VERSION_SIZE]; char *s, *d; - strcpy(prefix, "uca"); + strncpy(prefix, "uca", sizeof(prefix)); for (s = uca->version, d = prefix + strlen(prefix); *s; s++) { if ((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'z')) *d++ = *s; } diff --git a/mysql_strings/uca9-dump.cc b/mysql_strings/uca9-dump.cc index f1878be3..d665be71 100644 --- a/mysql_strings/uca9-dump.cc +++ b/mysql_strings/uca9-dump.cc @@ -332,7 +332,7 @@ static char *prefix_name(const MY_UCA *uca) { static char prefix[MY_UCA_VERSION_SIZE]; const char *s; char *d; - strcpy(prefix, "uca"); + strncpy(prefix, "uca", sizeof(prefix)); for (s = uca->version, d = prefix + strlen(prefix); *s; s++) { if ((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'z')) *d++ = *s; } diff --git a/mysql_strings/uctypedump.cc b/mysql_strings/uctypedump.cc index 8dc8caa4..86860eb0 100644 --- a/mysql_strings/uctypedump.cc +++ b/mysql_strings/uctypedump.cc @@ -138,7 +138,7 @@ static void load_unidata(MY_UNIDATA_PARAM *prm, MY_UNIDATA_CHAR *chr) { strncpy(tok, s, (unsigned int)(e - s)); tok[e - s] = 0; } else { - strcpy(tok, s); + strncpy(tok, s, sizeof(tok)); } end = tok + strlen(tok); @@ -217,7 +217,7 @@ static void unidata_char_set_cjk(MY_UNIDATA_CHAR *unidata, int max_char, if (cur_char < max_char) { MY_UNIDATA_CHAR *ch = &unidata[cur_char]; ch->mysql_ctype = _MY_L | _MY_U; - strcpy(ch->general_category, "Lo"); + strncpy(ch->general_category, "Lo", sizeof(ch->general_category)); } } @@ -320,7 +320,7 @@ static void dump_ctype(MY_UNIDATA_PARAM *prm, MY_UNIDATA_CHAR *unidata) { char page_name[128] = "NULL"; int ctype; if ((ctype = page_ctype(unidata + page * 256, 256)) < 0) { - sprintf(page_name, "uctype%s_page%02X", prm->varname, page); + snprintf(page_name, sizeof(page_name), "uctype%s_page%02X", prm->varname, page); ctype = 0; } printf("\t{%d,%s}%s\n", ctype, page_name, page < max_page - 1 ? "," : ""); diff --git a/mysql_strings/xml.cc b/mysql_strings/xml.cc index 8184baed..69a57761 100644 --- a/mysql_strings/xml.cc +++ b/mysql_strings/xml.cc @@ -303,9 +303,9 @@ static int my_xml_leave(MY_XML_PARSER *p, const char *str, size_t slen) { mstr(s, str, sizeof(s) - 1, slen); if (glen) { mstr(g, e + 1, sizeof(g) - 1, glen), - sprintf(p->errstr, "'' unexpected ('' wanted)", s, g); + snprintf(p->errstr, sizeof(p->errstr), "'' unexpected ('' wanted)", s, g); } else - sprintf(p->errstr, "'' unexpected (END-OF-INPUT wanted)", s); + snprintf(p->errstr, sizeof(p->errstr), "'' unexpected (END-OF-INPUT wanted)", s); return MY_XML_ERROR; } @@ -351,7 +351,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { if (MY_XML_SLASH == lex) { if (MY_XML_IDENT != (lex = my_xml_scan(p, &a))) { - sprintf(p->errstr, "%s unexpected (ident wanted)", lex2str(lex)); + snprintf(p->errstr, sizeof(p->errstr), "%s unexpected (ident wanted)", lex2str(lex)); return MY_XML_ERROR; } if (MY_XML_OK != my_xml_leave(p, a.beg, (size_t)(a.end - a.beg))) @@ -373,7 +373,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { if (MY_XML_OK != my_xml_enter(p, a.beg, (size_t)(a.end - a.beg))) return MY_XML_ERROR; } else { - sprintf(p->errstr, "%s unexpected (ident or '/' wanted)", lex2str(lex)); + snprintf(p->errstr, sizeof(p->errstr), "%s unexpected (ident or '/' wanted)", lex2str(lex)); return MY_XML_ERROR; } @@ -391,8 +391,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { (MY_XML_OK != my_xml_leave(p, a.beg, (size_t)(a.end - a.beg)))) return MY_XML_ERROR; } else { - sprintf(p->errstr, "%s unexpected (ident or string wanted)", - lex2str(lex)); + snprintf(p->errstr, sizeof(p->errstr), "%s unexpected (ident or string wanted)", lex2str(lex)); return MY_XML_ERROR; } } else if (MY_XML_IDENT == lex) { @@ -419,7 +418,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { gt: if (question) { if (lex != MY_XML_QUESTION) { - sprintf(p->errstr, "%s unexpected ('?' wanted)", lex2str(lex)); + snprintf(p->errstr, sizeof(p->errstr), "%s unexpected ('?' wanted)", lex2str(lex)); return MY_XML_ERROR; } if (MY_XML_OK != my_xml_leave(p, nullptr, 0)) return MY_XML_ERROR; @@ -431,7 +430,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { } if (lex != MY_XML_GT) { - sprintf(p->errstr, "%s unexpected ('>' wanted)", lex2str(lex)); + snprintf(p->errstr, sizeof(p->errstr), "%s unexpected ('>' wanted)", lex2str(lex)); return MY_XML_ERROR; } } else { @@ -449,7 +448,7 @@ int my_xml_parse(MY_XML_PARSER *p, const char *str, size_t len) { } if (p->attr.start[0]) { - sprintf(p->errstr, "unexpected END-OF-INPUT"); + snprintf(p->errstr, sizeof(p->errstr), "unexpected END-OF-INPUT"); return MY_XML_ERROR; } return MY_XML_OK; diff --git a/packaging/debian/CMakeLists.txt b/packaging/debian/CMakeLists.txt index 1012ec56..99e67bb6 100644 --- a/packaging/debian/CMakeLists.txt +++ b/packaging/debian/CMakeLists.txt @@ -1,3 +1,5 @@ +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/packaging/debian/mysql-connector-odbc-setup.postinst.in b/packaging/debian/mysql-connector-odbc-setup.postinst.in index bbda102e..41950d42 100644 --- a/packaging/debian/mysql-connector-odbc-setup.postinst.in +++ b/packaging/debian/mysql-connector-odbc-setup.postinst.in @@ -1,4 +1,6 @@ #!/bin/sh +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/packaging/debian/mysql-connector-odbc-setup.prerm.in b/packaging/debian/mysql-connector-odbc-setup.prerm.in index 022540bc..7d7ce4d1 100644 --- a/packaging/debian/mysql-connector-odbc-setup.prerm.in +++ b/packaging/debian/mysql-connector-odbc-setup.prerm.in @@ -1,4 +1,6 @@ #!/bin/sh +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/packaging/debian/mysql-connector-odbc.postinst.in b/packaging/debian/mysql-connector-odbc.postinst.in index ef2a9b1d..014cd4d2 100644 --- a/packaging/debian/mysql-connector-odbc.postinst.in +++ b/packaging/debian/mysql-connector-odbc.postinst.in @@ -1,4 +1,6 @@ #!/bin/sh +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify diff --git a/scripts/macosx/ReadMe.html.in b/scripts/macosx/ReadMe.html.in index e9791e4f..9af76db8 100644 --- a/scripts/macosx/ReadMe.html.in +++ b/scripts/macosx/ReadMe.html.in @@ -45,8 +45,8 @@

    This installer will:

      -
    1. Copy the ODBC driver and setup libraries to /usr/local/lib/.
    2. -
    3. Copy command-line utilities to /usr/local/bin/.
    4. +
    5. Copy the ODBC driver and setup libraries to @CPACK_PACKAGING_INSTALL_PREFIX@/lib/.
    6. +
    7. Copy command-line utilities to @CPACK_PACKAGING_INSTALL_PREFIX@/bin/.
    8. Register the driver and setup libraries with the iODBC driver manager.

    @@ -54,9 +54,9 @@

    The library files are;

      -
    • /usr/local/lib/libmyodbc8w.* (Unicode version)
    • -
    • /usr/local/lib/libmyodbc8a.* (ANSI version)
    • -
    • /usr/local/lib/libmyodbc8S.*
    • +
    • @CPACK_PACKAGING_INSTALL_PREFIX@/lib/libmyodbc8w.* (Unicode version)
    • +
    • @CPACK_PACKAGING_INSTALL_PREFIX@/lib/libmyodbc8a.* (ANSI version)
    • +
    • @CPACK_PACKAGING_INSTALL_PREFIX@/lib/libmyodbc8S.*

    @@ -66,16 +66,6 @@ The command-line utility file are;
  2. /usr/local/bin/myodbc-installer
  3. -

    - This package can be manually removed from your - system by removing all of the files installed - (see above) and by doing the following; -

    - -
    -  $ sudo rm -r /Library/Receipts/MyODBC*
    -
    - diff --git a/scripts/macosx/postflight.in b/scripts/macosx/postflight.in index 18199e65..75e72677 100644 --- a/scripts/macosx/postflight.in +++ b/scripts/macosx/postflight.in @@ -1,5 +1,7 @@ #!/bin/sh +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify @@ -50,8 +52,8 @@ # application. # ---------------------------------------------------------------------- -libdir=@INSTALLDIR@/lib -bindir=@INSTALLDIR@/bin +libdir=@CPACK_PACKAGING_INSTALL_PREFIX@/lib +bindir=@CPACK_PACKAGING_INSTALL_PREFIX@/bin for admdir in ~/Library/ODBC /Library/ODBC do diff --git a/setupgui/CMakeLists.txt b/setupgui/CMakeLists.txt index 5ee24adf..63988aaa 100644 --- a/setupgui/CMakeLists.txt +++ b/setupgui/CMakeLists.txt @@ -1,3 +1,5 @@ +# Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# # Copyright (c) 2007, 2020, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify @@ -26,8 +28,6 @@ # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -########################################################################## - add_definitions(-DUNICODE -D_UNICODE) if(UNICODE OR NOT ANSI) diff --git a/setupgui/ConfigDSN.cc b/setupgui/ConfigDSN.cc index 471b8859..8e79d437 100644 --- a/setupgui/ConfigDSN.cc +++ b/setupgui/ConfigDSN.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -162,7 +164,7 @@ BOOL INSTAPI ConfigDSNW(HWND hWnd, WORD nRequest, LPCWSTR pszDriver, hWnd means we will at least try to prompt, at which point the driver lib will be replaced by the name */ - ds_set_strattr(&ds->driver, driver->lib); + ds_set_wstrattr(&ds->driver, driver->lib); } else { @@ -170,7 +172,7 @@ BOOL INSTAPI ConfigDSNW(HWND hWnd, WORD nRequest, LPCWSTR pszDriver, no hWnd is a likely a call from an app w/no prompting so we put the driver name immediately */ - ds_set_strattr(&ds->driver, driver->name); + ds_set_wstrattr(&ds->driver, driver->name); } case ODBC_CONFIG_DSN: diff --git a/setupgui/callbacks.cc b/setupgui/callbacks.cc index 1383b151..72fc70f7 100644 --- a/setupgui/callbacks.cc +++ b/setupgui/callbacks.cc @@ -1,3 +1,5 @@ +// Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// // Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. // // This program is free software; you can redistribute it and/or modify @@ -276,7 +278,7 @@ void syncTabsData(HWND hwnd, DataSource *params) GET_BOOL_TAB(CONNECTION_TAB, dont_prompt_upon_connect); GET_BOOL_TAB(CONNECTION_TAB, auto_reconnect); GET_BOOL_TAB(CONNECTION_TAB, allow_multiple_statements); - GET_BOOL_TAB(CONNECTION_TAB, clientinteractive); + GET_BOOL_TAB(CONNECTION_TAB, client_interactive); GET_BOOL_TAB(CONNECTION_TAB, can_handle_exp_pwd); GET_BOOL_TAB(CONNECTION_TAB, enable_cleartext_plugin); GET_BOOL_TAB(CONNECTION_TAB, get_server_public_key); @@ -298,7 +300,38 @@ void syncTabsData(HWND hwnd, DataSource *params) GET_STRING_TAB(MFA_TAB, pwd3); #endif - /* 3 - Metadata*/ + /* 3 - Failover */ + GET_BOOL_TAB(FAILOVER_TAB, enable_cluster_failover); + GET_BOOL_TAB(FAILOVER_TAB, allow_reader_connections); + GET_BOOL_TAB(FAILOVER_TAB, gather_perf_metrics); + if (READ_BOOL_TAB(FAILOVER_TAB, gather_perf_metrics)) + { + GET_BOOL_TAB(FAILOVER_TAB, gather_metrics_per_instance); + } + + GET_STRING_TAB(FAILOVER_TAB, host_pattern); + GET_STRING_TAB(FAILOVER_TAB, cluster_id); + GET_UNSIGNED_TAB(FAILOVER_TAB, topology_refresh_rate); + GET_UNSIGNED_TAB(FAILOVER_TAB, failover_timeout); + GET_UNSIGNED_TAB(FAILOVER_TAB, failover_topology_refresh_rate); + GET_UNSIGNED_TAB(FAILOVER_TAB, failover_writer_reconnect_interval); + GET_UNSIGNED_TAB(FAILOVER_TAB, failover_reader_connect_timeout); + GET_UNSIGNED_TAB(FAILOVER_TAB, connect_timeout); + GET_UNSIGNED_TAB(FAILOVER_TAB, network_timeout); + + /* 4 - Monitoring */ + GET_BOOL_TAB(MONITORING_TAB, enable_failure_detection); + if (READ_BOOL_TAB(MONITORING_TAB, enable_failure_detection)) + { + GET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_time); + GET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_timeout); + GET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_interval); + GET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_count); + GET_UNSIGNED_TAB(MONITORING_TAB, monitor_disposal_time); + } + + + /* 5 - Metadata */ GET_BOOL_TAB(METADATA_TAB, change_bigint_columns_to_int); GET_BOOL_TAB(METADATA_TAB, handle_binary_as_char); GET_BOOL_TAB(METADATA_TAB, return_table_names_for_SqlDescribeCol); @@ -306,7 +339,7 @@ void syncTabsData(HWND hwnd, DataSource *params) GET_BOOL_TAB(METADATA_TAB, no_schema); GET_BOOL_TAB(METADATA_TAB, limit_column_size); - /* 4 - Cursors/Results */ + /* 6 - Cursors/Results */ GET_BOOL_TAB(CURSORS_TAB, return_matching_rows); GET_BOOL_TAB(CURSORS_TAB, auto_increment_null_search); GET_BOOL_TAB(CURSORS_TAB, dynamic_cursor); @@ -324,10 +357,10 @@ void syncTabsData(HWND hwnd, DataSource *params) { params->cursor_prefetch_number= 0; } - /* 5 - debug*/ + /* 7 - debug*/ GET_BOOL_TAB(DEBUG_TAB,save_queries); - /* 6 - ssl related */ + /* 8 - ssl related */ GET_STRING_TAB(SSL_TAB, sslkey); GET_STRING_TAB(SSL_TAB, sslcert); GET_STRING_TAB(SSL_TAB, sslca); @@ -342,7 +375,7 @@ void syncTabsData(HWND hwnd, DataSource *params) GET_STRING_TAB(SSL_TAB, ssl_crl); GET_STRING_TAB(SSL_TAB, ssl_crlpath); - /* 7 - Misc*/ + /* 9 - Misc*/ GET_BOOL_TAB(MISC_TAB, safe); GET_BOOL_TAB(MISC_TAB, dont_use_set_locale); GET_BOOL_TAB(MISC_TAB, ignore_space_after_function_names); @@ -354,6 +387,7 @@ void syncTabsData(HWND hwnd, DataSource *params) GET_BOOL_TAB(MISC_TAB, no_date_overflow); GET_BOOL_TAB(MISC_TAB, enable_local_infile); GET_STRING_TAB(MISC_TAB, load_data_local_dir); + } /* @@ -368,7 +402,7 @@ void syncTabs(HWND hwnd, DataSource *params) SET_BOOL_TAB(CONNECTION_TAB, auto_reconnect); SET_BOOL_TAB(CONNECTION_TAB, enable_dns_srv); SET_BOOL_TAB(CONNECTION_TAB, allow_multiple_statements); - SET_BOOL_TAB(CONNECTION_TAB, clientinteractive); + SET_BOOL_TAB(CONNECTION_TAB, client_interactive); SET_BOOL_TAB(CONNECTION_TAB, can_handle_exp_pwd); SET_BOOL_TAB(CONNECTION_TAB, enable_cleartext_plugin); SET_BOOL_TAB(CONNECTION_TAB, get_server_public_key); @@ -380,10 +414,10 @@ void syncTabs(HWND hwnd, DataSource *params) #endif { SET_COMBO_TAB(CONNECTION_TAB, charset); - SET_STRING_TAB(CONNECTION_TAB,initstmt); - SET_STRING_TAB(CONNECTION_TAB,plugin_dir); - SET_STRING_TAB(CONNECTION_TAB,default_auth); - SET_STRING_TAB(CONNECTION_TAB,oci_config_file); + SET_STRING_TAB(CONNECTION_TAB, initstmt); + SET_STRING_TAB(CONNECTION_TAB, plugin_dir); + SET_STRING_TAB(CONNECTION_TAB, default_auth); + SET_STRING_TAB(CONNECTION_TAB, oci_config_file); } #if MFA_ENABLED @@ -392,7 +426,75 @@ void syncTabs(HWND hwnd, DataSource *params) SET_STRING_TAB(MFA_TAB, pwd3); #endif - /* 3 - Metadata*/ + /* 3 - Failover */ + SET_BOOL_TAB(FAILOVER_TAB, enable_cluster_failover); + SET_BOOL_TAB(FAILOVER_TAB, allow_reader_connections); + SET_BOOL_TAB(FAILOVER_TAB, gather_perf_metrics); + if(READ_BOOL_TAB(FAILOVER_TAB, gather_perf_metrics)) + { +#ifdef _WIN32 + SET_ENABLED(FAILOVER_TAB, IDC_CHECK_gather_metrics_per_instance, TRUE); +#endif + SET_CHECKED_TAB(FAILOVER_TAB, gather_perf_metrics, TRUE); + SET_BOOL_TAB(FAILOVER_TAB, gather_metrics_per_instance); + } + + SET_STRING_TAB(FAILOVER_TAB, host_pattern); + SET_STRING_TAB(FAILOVER_TAB, cluster_id); + + if (params->topology_refresh_rate > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, topology_refresh_rate); + } + + if (params->failover_timeout > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, failover_timeout); + } + + if (params->failover_topology_refresh_rate > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, failover_topology_refresh_rate); + } + + if (params->failover_writer_reconnect_interval > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, failover_writer_reconnect_interval); + } + + if (params->failover_reader_connect_timeout > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, failover_reader_connect_timeout); + } + + if (params->connect_timeout > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, connect_timeout); + } + + if (params->network_timeout > 0) + { + SET_UNSIGNED_TAB(FAILOVER_TAB, network_timeout); + } + + /* 4 - Monitoring */ + SET_BOOL_TAB(MONITORING_TAB, enable_failure_detection); + if (READ_BOOL_TAB(MONITORING_TAB, enable_failure_detection)) { +#ifdef _WIN32 + SET_ENABLED(MONITORING_TAB, IDC_EDIT_failure_detection_time, TRUE); + SET_ENABLED(MONITORING_TAB, IDC_EDIT_failure_detection_interval, TRUE); + SET_ENABLED(MONITORING_TAB, IDC_EDIT_failure_detection_count, TRUE); + SET_ENABLED(MONITORING_TAB, IDC_EDIT_monitor_disposal_time, TRUE); + SET_ENABLED(MONITORING_TAB, IDC_EDIT_failure_detection_timeout, TRUE); +#endif + SET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_time); + SET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_interval); + SET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_count); + SET_UNSIGNED_TAB(MONITORING_TAB, monitor_disposal_time); + SET_UNSIGNED_TAB(MONITORING_TAB, failure_detection_timeout); + } + + /* 5 - Metadata */ SET_BOOL_TAB(METADATA_TAB, change_bigint_columns_to_int); SET_BOOL_TAB(METADATA_TAB, handle_binary_as_char); SET_BOOL_TAB(METADATA_TAB, return_table_names_for_SqlDescribeCol); @@ -400,7 +502,7 @@ void syncTabs(HWND hwnd, DataSource *params) SET_BOOL_TAB(METADATA_TAB, no_schema); SET_BOOL_TAB(METADATA_TAB, limit_column_size); - /* 4 - Cursors/Results */ + /* 6 - Cursors/Results */ SET_BOOL_TAB(CURSORS_TAB, return_matching_rows); SET_BOOL_TAB(CURSORS_TAB, auto_increment_null_search); SET_BOOL_TAB(CURSORS_TAB, dynamic_cursor); @@ -419,10 +521,10 @@ void syncTabs(HWND hwnd, DataSource *params) SET_UNSIGNED_TAB(CURSORS_TAB, cursor_prefetch_number); } - /* 5 - debug*/ + /* 7 - debug*/ SET_BOOL_TAB(DEBUG_TAB,save_queries); - /* 6 - ssl related */ + /* 8 - ssl related */ #ifdef _WIN32 if ( getTabCtrlTabPages(SSL_TAB-1) ) #endif @@ -460,7 +562,7 @@ void syncTabs(HWND hwnd, DataSource *params) SET_STRING_TAB(SSL_TAB, tls_versions); } - /* 7 - Misc*/ + /* 9 - Misc*/ SET_BOOL_TAB(MISC_TAB, safe); SET_BOOL_TAB(MISC_TAB, dont_use_set_locale); SET_BOOL_TAB(MISC_TAB, ignore_space_after_function_names); @@ -472,6 +574,7 @@ void syncTabs(HWND hwnd, DataSource *params) SET_BOOL_TAB(MISC_TAB, no_date_overflow); SET_BOOL_TAB(MISC_TAB, enable_local_infile); SET_STRING_TAB(MISC_TAB, load_data_local_dir); + } void FillParameters(HWND hwnd, DataSource *params) diff --git a/setupgui/gtk/ODBCINSTGetProperties.cc b/setupgui/gtk/ODBCINSTGetProperties.cc index 1d356bfe..583eae7e 100644 --- a/setupgui/gtk/ODBCINSTGetProperties.cc +++ b/setupgui/gtk/ODBCINSTGetProperties.cc @@ -46,7 +46,7 @@ static const char *MYODBC_OPTIONS[][3] = { {"SOCKET", "T", "The Unix socket file if SERVER=localhost"}, {"INITSTMT", "T", "Initial statement executed at the connecting time"}, {"CHARSET", "T", "The character set to use for the connection"}, - {"PREFETCH", "T", "Prefecth from server by N rows at a time"}, + {"PREFETCH", "T", "Prefetch from server by N rows at a time"}, {"READTIMEOUT", "T", "The timeout in seconds for attempts to read from the server"}, {"WRITETIMEOUT", "T", "The timeout in seconds for attempts to write to the server"}, {"SSLCA", "F", "The path to a file with a list of trust SSL CAs"}, @@ -71,7 +71,7 @@ static const char *MYODBC_OPTIONS[][3] = { {"USE_MYCNF", "C", "Read options from my.cnf"}, {"SAFE", "C", "Add some extra safety checks"}, {"NO_TRANSACTIONS", "C", "Disable transaction support"}, - {"LOG_QUERY", "C", "Log queries to %TEMP%\myodbc.sql"}, + {"LOG_QUERY", "C", "Log driver activity to %TEMP%\myodbc.log"}, {"NO_CACHE", "C", "Don't cache results of forward-only cursors"}, {"FORWARD_CURSOR", "C", "Force use of forward-only cursors"}, {"AUTO_RECONNECT", "C", "Enable automatic reconnect"}, diff --git a/setupgui/gtk/odbc.glade b/setupgui/gtk/odbc.glade index d740f518..ccc1da6a 100644 --- a/setupgui/gtk/odbc.glade +++ b/setupgui/gtk/odbc.glade @@ -1,6 +1,8 @@