aws_advanced_ruby_driver_wrapper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. metadata +227 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'concurrent'
18
+ require_relative '../errors'
19
+ require_relative '../host/host_info'
20
+ require_relative '../host/host_availability'
21
+
22
+ module AwsAdvancedRubyDriverWrapper
23
+ module Plugins
24
+ class DefaultPlugin
25
+ SUBSCRIBED_METHODS = Set['*'].freeze
26
+ HOST_PORT_KEYS = %i[host port].freeze
27
+
28
+ def initialize(service_container, _props)
29
+ @service_container = service_container
30
+ end
31
+
32
+ def subscribed_methods
33
+ SUBSCRIBED_METHODS
34
+ end
35
+
36
+ def connect(host_info, driver_props, is_initial_connection, _pipeline_callable)
37
+ connection_service = @service_container.connection_service
38
+ driver_dialect = @service_container.dialect_service.driver_dialect
39
+ target_host_info = if is_initial_connection && connection_service.multi_host_url?
40
+ # If the user specified a multi-host URL, we should always pass the same hosts/ports they
41
+ # specified. Note that host_info may have a different value than initial_host_info.
42
+ connection_service.initial_host_info
43
+ else
44
+ host_info
45
+ end
46
+
47
+ conn = driver_dialect.connect(target_host_info, driver_props)
48
+
49
+ if conn.nil?
50
+ raise Errors::AwsError,
51
+ "Failed to connect to #{target_host_info&.host}: driver returned nil connection"
52
+ end
53
+
54
+ # If host was not specified (Unix socket / localhost), fill in from the live connection.
55
+ if host_info.nil? || !host_info.host_specified?
56
+ host_info = Host::HostInfo.new(
57
+ host: conn.respond_to?(:host) && conn.host ? conn.host : Host::HostInfo::NO_HOST,
58
+ port: conn.respond_to?(:port) && conn.port ? conn.port.to_s : Host::HostInfo::NO_PORT
59
+ )
60
+ else
61
+ @service_container.host_service.set_availability(host_info, Host::HostAvailability::AVAILABLE)
62
+ end
63
+
64
+ connection_service.update_current_connection(conn, host_info)
65
+
66
+ if is_initial_connection
67
+ @service_container.dialect_service.update_dialect(conn)
68
+ @service_container.host_service.refresh_host_list
69
+
70
+ if connection_service.pg? && connection_service.multi_host_url?
71
+ connection_service.config.initial_host_info = Host::HostInfo.new(
72
+ host: conn.host,
73
+ port: conn.port.to_i
74
+ )
75
+ end
76
+ end
77
+
78
+ init_func = AwsAdvancedRubyDriverWrapper.config.connection_init_func
79
+ init_func&.call(conn, host_info)
80
+
81
+ conn
82
+ end
83
+
84
+ def internal_connect(host_info, driver_props, _wrapper_props, _is_initial_connection, _pipeline_callable)
85
+ driver_dialect = @service_container.dialect_service.driver_dialect
86
+ driver_dialect.connect(host_info, driver_props)
87
+ end
88
+
89
+ def execute(target_method_name, target_callable, *args, **, &)
90
+ session = @service_container.session_state_service
91
+ autocommit_before = session&.autocommit?
92
+ succeeded = false
93
+
94
+ begin
95
+ result = target_callable.call(*args, **, &)
96
+ succeeded = true
97
+ result
98
+ ensure
99
+ if session && (conn = @service_container.connection_service.current_connection)
100
+ dialect = @service_container.dialect_service.driver_dialect
101
+ session.update_transaction_state(target_method_name, args, autocommit_before,
102
+ dialect, conn, succeeded: succeeded)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module FailoverMode
20
+ STRICT_WRITER = :strict_writer
21
+ STRICT_READER = :strict_reader
22
+ READER_OR_WRITER = :reader_or_writer
23
+
24
+ def self.from_value(value)
25
+ return nil if value.nil?
26
+
27
+ normalized = value.to_s.strip.downcase
28
+ return nil if normalized.empty?
29
+
30
+ case normalized
31
+ when 'strict_writer', 'strict-writer', 'strictwriter'
32
+ STRICT_WRITER
33
+ when 'strict_reader', 'strict-reader', 'strictreader'
34
+ STRICT_READER
35
+ when 'reader_or_writer', 'reader-or-writer', 'readerorwriter'
36
+ READER_OR_WRITER
37
+ else
38
+ raise ArgumentError, "Invalid failover mode: '#{value}'"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'concurrent'
18
+ require_relative '../errors'
19
+ require_relative '../host/host_info'
20
+ require_relative '../host/host_role'
21
+ require_relative '../host/host_availability'
22
+ require_relative '../logging'
23
+ require_relative '../utils/rds_utils'
24
+ require_relative '../utils/rds_url_type'
25
+ require_relative '../utils/retry_util'
26
+ require_relative '../wrapper_property'
27
+ require_relative 'failover_mode'
28
+
29
+ module AwsAdvancedRubyDriverWrapper
30
+ module Plugins
31
+ class FailoverPlugin
32
+ include Logging
33
+
34
+ ReaderFailoverResult = Data.define(:connection, :host_info)
35
+
36
+ def initialize(service_container, props = ::Concurrent::Map.new)
37
+ @service_container = service_container
38
+ @wrapper_props = props
39
+
40
+ @retry_util = Utils::RetryUtil.new(service_container)
41
+ @failover_timeout = PropertyDefinition::FAILOVER_TIMEOUT_SEC.get_float(props)
42
+ @reader_selector_strategy = PropertyDefinition::FAILOVER_READER_HOST_SELECTOR_STRATEGY.get(props)
43
+ @failover_mode = nil
44
+ @rds_url_type = nil
45
+ @closed_explicitly = false
46
+
47
+ network_methods = @service_container.dialect_service.driver_dialect.network_bound_methods
48
+ @subscribed_methods = (Set['connect'] | network_methods).freeze
49
+ end
50
+
51
+ attr_reader :subscribed_methods
52
+
53
+ def connect(host_info, driver_props, is_initial_connection, pipeline_callable)
54
+ init_failover_mode
55
+
56
+ unless PropertyDefinition::ENABLE_CONNECT_FAILOVER.get_bool(@wrapper_props)
57
+ return verified_connection(is_initial_connection, host_info, driver_props, pipeline_callable)
58
+ end
59
+
60
+ topology_host = host_service.hosts.find { |h| h.host_and_port == host_info&.host_and_port }
61
+
62
+ if !topology_host.nil? && topology_host.availability == Host::HostAvailability::UNAVAILABLE
63
+ host_service.refresh_host_list
64
+ return connect_via_failover(is_initial_connection)
65
+ end
66
+
67
+ begin
68
+ conn = verified_connection(is_initial_connection, host_info, driver_props, pipeline_callable)
69
+ host_service.refresh_host_list if is_initial_connection
70
+ conn
71
+ rescue StandardError => e
72
+ raise unless trigger_failover?(e)
73
+
74
+ host_service.set_availability(host_info, Host::HostAvailability::UNAVAILABLE)
75
+ connect_via_failover(is_initial_connection)
76
+ end
77
+ end
78
+
79
+ def execute(method_name, pipeline_callable, *, **, &)
80
+ if can_direct_execute?(method_name)
81
+ @closed_explicitly = true if method_name == RubyMethod::CONNECTION_CLOSE.name
82
+ return pipeline_callable.call
83
+ end
84
+
85
+ conn = connection_service.current_connection
86
+ if conn.nil?
87
+ logger.warn do
88
+ "[execute] current_connection is nil for #{method_name} — " \
89
+ "host_info=#{connection_service.current_host_info&.host}, " \
90
+ "closed_explicitly=#{@closed_explicitly}, failover_mode=#{@failover_mode.inspect}"
91
+ end
92
+ elsif !@closed_explicitly && driver_dialect.closed?(conn)
93
+ logger.warn("#{method_name} was called on closed connection #{conn} to #{connection_service.current_host_info}. " \
94
+ 'The driver will attempt to failover and then execute.')
95
+ failover
96
+ end
97
+
98
+ begin
99
+ pipeline_callable.call
100
+ rescue StandardError => e
101
+ handle_error(e)
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def connect_via_failover(is_initial_connection)
108
+ begin
109
+ failover
110
+ rescue Errors::FailoverSuccessError => _e
111
+ conn = connection_service.current_connection
112
+ end
113
+
114
+ host_service.refresh_host_list if is_initial_connection
115
+ conn
116
+ end
117
+
118
+ def connection_service
119
+ @service_container.connection_service
120
+ end
121
+
122
+ def host_service
123
+ @service_container.host_service
124
+ end
125
+
126
+ def dialect_service
127
+ @service_container.dialect_service
128
+ end
129
+
130
+ def driver_dialect
131
+ dialect_service.driver_dialect
132
+ end
133
+
134
+ def db_dialect
135
+ dialect_service.db_dialect
136
+ end
137
+
138
+ def init_failover_mode
139
+ return unless @rds_url_type.nil?
140
+
141
+ @failover_mode = FailoverMode.from_value(PropertyDefinition::FAILOVER_MODE.get(@wrapper_props))
142
+ initial_host = connection_service.initial_host_info
143
+
144
+ @rds_url_type = Utils::RdsUtils.identify_rds_type(initial_host&.host)
145
+ reject_rds_proxy_endpoint
146
+
147
+ if @failover_mode.nil?
148
+ @failover_mode = if @rds_url_type == Utils::RdsUrlType::RDS_READER_CLUSTER
149
+ FailoverMode::READER_OR_WRITER
150
+ else
151
+ FailoverMode::STRICT_WRITER
152
+ end
153
+ end
154
+
155
+ logger.debug { "failover_mode=#{@failover_mode}" }
156
+ end
157
+
158
+ # RDS Proxy handles failover internally, so a failover plugin must not be layered on top of it.
159
+ #
160
+ # @raise [Errors::AwsError] if the initial endpoint is an RDS Proxy endpoint
161
+ def reject_rds_proxy_endpoint
162
+ return unless [Utils::RdsUrlType::RDS_PROXY, Utils::RdsUrlType::RDS_PROXY_ENDPOINT].include?(@rds_url_type)
163
+
164
+ raise Errors::AwsError,
165
+ 'The failover plugin is not compatible with RDS Proxy endpoints. ' \
166
+ 'RDS Proxy handles failover internally - please remove the failover plugin from your configuration.'
167
+ end
168
+
169
+ def can_direct_execute?(method_name)
170
+ method_name == RubyMethod::CONNECTION_CLOSE.name ||
171
+ method_name == RubyMethod::CONNECTION_PING.name
172
+ end
173
+
174
+ def handle_error(error)
175
+ logger.debug { "Detected error: #{error.message}" }
176
+ raise error unless trigger_failover?(error)
177
+
178
+ invalidate_current_connection
179
+ host_service.set_availability(
180
+ connection_service.current_host_info,
181
+ Host::HostAvailability::UNAVAILABLE
182
+ )
183
+
184
+ failover
185
+ # Failover will be skipped if the connection was explicitly closed. In this case we raise the error here.
186
+ raise error
187
+ end
188
+
189
+ def trigger_failover?(error)
190
+ if host_service.all_hosts.empty?
191
+ logger.debug do
192
+ "Skipping failover for #{connection_service.current_host_info}: no topology available."
193
+ end
194
+ return false
195
+ end
196
+
197
+ return true if dialect_service.network_error?(error)
198
+
199
+ dialect_service.read_only_error?(error) && failover_on_read_only_error?
200
+ end
201
+
202
+ # Whether a read-only error should trigger failover. It should only do so when failover would
203
+ # target a writer, since a read-only error means the current connection is not one.
204
+ #
205
+ # @return [Boolean]
206
+ def failover_on_read_only_error?
207
+ @failover_mode == FailoverMode::STRICT_WRITER
208
+ end
209
+
210
+ # Rolls back an open transaction on the current connection so it does not linger server-side.
211
+ #
212
+ # The broken connection is intentionally left open rather than closed here. A real pg/mysql2
213
+ # connection is left broken-but-open on network failure; its owner (ActiveRecord via
214
+ # disconnect!/discard!, or a raw-driver caller) is responsible for closing it. Closing it eagerly
215
+ # mid-error-handling would leave the facade pointing at a freed object on a failed failover, so
216
+ # introspection calls AR makes during its error path (e.g. transaction_status, ping) would raise
217
+ # instead of reporting a broken state. On a successful failover the old connection is closed when
218
+ # the new one is swapped in via ConnectionService#update_current_connection.
219
+ def invalidate_current_connection
220
+ conn = connection_service.current_connection
221
+ return if conn.nil?
222
+ return unless @service_container.session_state_service.in_transaction?
223
+
224
+ begin
225
+ driver_dialect.execute(conn, 'ROLLBACK')
226
+ rescue StandardError
227
+ nil
228
+ end
229
+ end
230
+
231
+ def failover
232
+ if @closed_explicitly
233
+ logger.debug { 'Connection was explicitly closed, skipping failover' }
234
+ return
235
+ end
236
+
237
+ if @failover_mode == FailoverMode::STRICT_WRITER
238
+ failover_writer
239
+ else
240
+ failover_reader
241
+ end
242
+ end
243
+
244
+ def failover_reader
245
+ failover_start = Time.now
246
+ failover_deadline = failover_start + @failover_timeout
247
+
248
+ logger.info { 'Starting reader failover' }
249
+
250
+ unless host_service.force_refresh_host_list?(verify_writer: false, timeout_sec: 0)
251
+ raise Errors::FailoverFailedError, 'The request to discover the new topology was unsuccessful'
252
+ end
253
+
254
+ begin
255
+ was_in_transaction = @service_container.session_state_service.in_transaction?
256
+ result = reader_failover_connection(failover_deadline)
257
+ raise Errors::FailoverFailedError, 'Unable to connect to a reader instance' unless result&.connection
258
+
259
+ connection_service.update_current_connection(result.connection, result.host_info)
260
+ rescue Timeout::Error
261
+ raise Errors::FailoverFailedError, 'Unable to connect to a reader instance'
262
+ end
263
+
264
+ raise_failover_success_error(was_in_transaction)
265
+ ensure
266
+ duration_ms = ((Time.now - failover_start) * 1000).round
267
+ logger.debug { "Reader failover duration: #{duration_ms}ms" }
268
+ end
269
+
270
+ def failover_writer
271
+ failover_start = Time.now
272
+ failover_deadline = failover_start + @failover_timeout
273
+ result = nil
274
+
275
+ logger.info { 'Starting writer failover' }
276
+
277
+ begin
278
+ unless host_service.force_refresh_host_list?(verify_writer: true, timeout_sec: @failover_timeout)
279
+ raise Errors::FailoverFailedError, 'The request to discover the new topology timed out or was unsuccessful'
280
+ end
281
+
282
+ was_in_transaction = @service_container.session_state_service.in_transaction?
283
+ result = @retry_util.connect_to_writer(self, @service_container.plugin_manager, deadline: failover_deadline)
284
+ raise Errors::FailoverFailedError, 'Unable to connect to a writer instance' unless result&.connection && result.host_info
285
+
286
+ connection_service.update_current_connection(result.connection, result.host_info)
287
+ rescue Timeout::Error
288
+ close_quietly(result&.connection)
289
+ raise Errors::FailoverFailedError,
290
+ "Writer failover timed out after #{@failover_timeout}s. Unable to connect to a new writer instance."
291
+ ensure
292
+ duration_ms = ((Time.now - failover_start) * 1000).round
293
+ logger.debug { "Writer failover duration: #{duration_ms}ms" }
294
+ end
295
+
296
+ raise_failover_success_error(was_in_transaction)
297
+ end
298
+
299
+ def raise_failover_success_error(was_in_transaction)
300
+ logger.debug { "Established connection to: #{connection_service.current_host_info}" }
301
+ raise Errors::FailoverSuccessError unless was_in_transaction
302
+
303
+ @service_container.session_state_service.in_transaction = false
304
+ raise Errors::TransactionStateUnknownError
305
+ end
306
+
307
+ def reader_failover_connection(deadline)
308
+ original_writer = nil
309
+ original_writer_still_writer = false
310
+
311
+ loop do
312
+ break if Time.now >= deadline
313
+
314
+ hosts = host_service.hosts
315
+ reader_candidates = hosts.select { |h| h.role == Host::HostRole::READER }
316
+ original_writer ||= hosts.find { |h| h.role == Host::HostRole::WRITER }
317
+
318
+ result = try_reader_candidates(reader_candidates, deadline)
319
+ return result if result
320
+
321
+ result = try_original_writer(original_writer, original_writer_still_writer)
322
+ case result
323
+ when ReaderFailoverResult
324
+ return result
325
+ when :still_writer
326
+ original_writer_still_writer = true
327
+ end
328
+
329
+ sleep(0.1)
330
+ end
331
+
332
+ raise Timeout::Error, 'The reader failover process was not able to establish a connection before timing out.'
333
+ end
334
+
335
+ def try_reader_candidates(reader_candidates, deadline)
336
+ remaining = reader_candidates.dup
337
+
338
+ while !remaining.empty? && Time.now < deadline
339
+ candidate = select_reader_candidate(remaining)
340
+ if candidate.nil?
341
+ # Unable to find available candidate in the host list. Let's try assuming all hosts are available.
342
+ available = remaining.map do |h|
343
+ h.deep_dup.tap { |dup| dup.availability = Host::HostAvailability::AVAILABLE }
344
+ end
345
+ candidate = select_reader_candidate(available)
346
+ end
347
+
348
+ if candidate.nil?
349
+ logger.debug { 'Unable to find reader in the updated host list.' }
350
+ break
351
+ end
352
+
353
+ outcome, result = attempt_reader_connection(candidate)
354
+ case outcome
355
+ when :success
356
+ return result
357
+ when :writer
358
+ reader_candidates.delete(candidate)
359
+ remaining.delete(candidate)
360
+ else
361
+ remaining.delete(candidate)
362
+ end
363
+ end
364
+
365
+ nil
366
+ end
367
+
368
+ def try_original_writer(original_writer, original_writer_still_writer)
369
+ return nil if original_writer.nil?
370
+ return nil if @failover_mode == FailoverMode::STRICT_READER && original_writer_still_writer
371
+
372
+ outcome, result = attempt_reader_connection(original_writer)
373
+ case outcome
374
+ when :success
375
+ result
376
+ when :writer
377
+ :still_writer
378
+ else
379
+ logger.debug { "Failed to connect to host: #{original_writer.url}" }
380
+ nil
381
+ end
382
+ end
383
+
384
+ def attempt_reader_connection(host_info)
385
+ conn = @service_container.plugin_manager.connect(host_info, connection_service.driver_props, false, plugin_to_skip: self)
386
+ # Since the roles in the host list might not be accurate, we execute a query to check the instance's role.
387
+ role = db_dialect.host_role(conn)
388
+
389
+ if role == Host::HostRole::READER || @failover_mode != FailoverMode::STRICT_READER
390
+ updated_host = host_info.deep_dup(role: role)
391
+ return [:success, ReaderFailoverResult.new(conn, updated_host)]
392
+ end
393
+
394
+ # The role is WRITER or UNKNOWN, and we are in STRICT_READER mode, so the connection is not valid.
395
+ close_quietly(conn)
396
+ if role == Host::HostRole::WRITER
397
+ [:writer, nil]
398
+ else
399
+ logger.debug do
400
+ "Unable to determine host role for #{host_info.url}. " \
401
+ 'Since failover mode is set to STRICT_READER and the host may be a writer, ' \
402
+ 'it will not be selected for reader failover.'
403
+ end
404
+ [:unknown, nil]
405
+ end
406
+ rescue StandardError
407
+ close_quietly(conn)
408
+ [:failed, nil]
409
+ end
410
+
411
+ def select_reader_candidate(hosts)
412
+ host_service.select_host(
413
+ hosts,
414
+ Host::HostRole::READER,
415
+ @reader_selector_strategy
416
+ )
417
+ rescue StandardError
418
+ nil
419
+ end
420
+
421
+ def verified_connection(is_initial_connection, host_info, driver_props, connect_func)
422
+ url_type = Utils::RdsUtils.identify_rds_type(host_info&.host)
423
+ if url_type != Utils::RdsUrlType::RDS_WRITER_CLUSTER
424
+ # We are not using a writer cluster endpoint. No verification needed - continue with the regular workflow.
425
+ return connect_func.call
426
+ end
427
+
428
+ conn = connect_func.call
429
+ if db_dialect.host_role(conn) == Host::HostRole::WRITER
430
+ host_service.refresh_host_list
431
+ return conn
432
+ end
433
+
434
+ # The writer cluster URL resolved to a reader. We will try to redirect to the writer instance.
435
+ host_service.force_refresh_host_list?(verify_writer: false, timeout_sec: 5.0)
436
+ writer = host_service.all_hosts.find { |h| h.role == Host::HostRole::WRITER }
437
+ if writer.nil? || Utils::RdsUtils.rds_cluster_dns?(writer.host)
438
+ # Writer instance endpoint not found - unable to redirect.
439
+ close_quietly(conn)
440
+ raise Errors::AwsError, 'Stale DNS detected - a writer was requested, but the writer cluster endpoint resolved to a reader'
441
+ end
442
+
443
+ allowed_hosts = host_service.hosts
444
+ unless allowed_hosts.any? { |h| h.host_and_port == writer.host_and_port }
445
+ raise Errors::AwsError, "Current writer #{writer.host_and_port} is not in allowed hosts"
446
+ end
447
+
448
+ # Attempt to correct the stale DNS problem by connecting to the writer instance.
449
+ logger.debug { "Stale DNS data detected. Opening a connection to #{writer.host}" }
450
+ writer_conn = @service_container.plugin_manager.connect(writer, driver_props, false, plugin_to_skip: self)
451
+ connection_service.initial_host_info = writer if is_initial_connection
452
+
453
+ # Close the incorrect reader connection.
454
+ close_quietly(conn)
455
+ writer_conn
456
+ end
457
+
458
+ def close_quietly(conn)
459
+ return if conn.nil?
460
+
461
+ driver_dialect.close_connection(conn)
462
+ rescue StandardError
463
+ # ignore
464
+ end
465
+ end
466
+ end
467
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module Gdb
20
+ # The host roles and regions that the GDB failover plugin may target during failover.
21
+ module GdbFailoverMode
22
+ STRICT_WRITER = :strict_writer
23
+ STRICT_HOME_READER = :strict_home_reader
24
+ STRICT_OUT_OF_HOME_READER = :strict_out_of_home_reader
25
+ STRICT_ANY_READER = :strict_any_reader
26
+ HOME_READER_OR_WRITER = :home_reader_or_writer
27
+ OUT_OF_HOME_READER_OR_WRITER = :out_of_home_reader_or_writer
28
+ ANY_READER_OR_WRITER = :any_reader_or_writer
29
+
30
+ ALL = [
31
+ STRICT_WRITER,
32
+ STRICT_HOME_READER,
33
+ STRICT_OUT_OF_HOME_READER,
34
+ STRICT_ANY_READER,
35
+ HOME_READER_OR_WRITER,
36
+ OUT_OF_HOME_READER_OR_WRITER,
37
+ ANY_READER_OR_WRITER
38
+ ].freeze
39
+
40
+ # Maps every accepted spelling of a mode to the mode itself. Kebab-case, snake_case and
41
+ # squashed spellings are all accepted, e.g. 'strict-home-reader', 'strict_home_reader'
42
+ # and 'stricthomereader'.
43
+ NAME_TO_VALUE = ALL.each_with_object({}) do |mode, mapping|
44
+ snake = mode.to_s
45
+ mapping[snake] = mode
46
+ mapping[snake.tr('_', '-')] = mode
47
+ mapping[snake.delete('_')] = mode
48
+ end.freeze
49
+
50
+ # Resolves a configured mode string into one of the mode constants.
51
+ #
52
+ # @param value [String, Symbol, nil] the configured value
53
+ # @return [Symbol, nil] the mode, or nil when no value was configured
54
+ # @raise [ArgumentError] if the value does not name a known mode
55
+ def self.from_value(value)
56
+ return nil if value.nil?
57
+
58
+ normalized = value.to_s.strip.downcase
59
+ return nil if normalized.empty?
60
+
61
+ NAME_TO_VALUE.fetch(normalized) do
62
+ raise ArgumentError, "Invalid global database failover mode: '#{value}'"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end