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,403 @@
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_relative '../failover_plugin'
18
+ require_relative '../../errors'
19
+ require_relative '../../host/host_role'
20
+ require_relative '../../property_definition'
21
+ require_relative '../../utils/accessible_regions'
22
+ require_relative '../../utils/rds_utils'
23
+ require_relative '../../utils/rds_url_type'
24
+ require_relative 'gdb_failover_mode'
25
+
26
+ module AwsAdvancedRubyDriverWrapper
27
+ module Plugins
28
+ module Gdb
29
+ # Failover plugin for Global Aurora Databases.
30
+ #
31
+ # Unlike {FailoverPlugin}, which always targets the same role, this plugin picks its target
32
+ # based on the region of the cluster that is currently the GDB primary. Two modes are
33
+ # configured: +in_home_failover_mode+ applies while the primary region is the home region,
34
+ # and +out_of_home_failover_mode+ applies while it is not. Both accept any of the modes in
35
+ # {GdbFailoverMode}.
36
+ #
37
+ # The home region is taken from +failover_home_region+, or derived from the initial endpoint
38
+ # when that endpoint carries a region. When +accessible_regions+ is set, hosts outside those
39
+ # regions are never selected.
40
+ class GdbFailoverPlugin < FailoverPlugin
41
+ def initialize(service_container, props = ::Concurrent::Map.new)
42
+ super
43
+
44
+ # The inherited @failover_mode is unused in this class; @in_home_failover_mode and
45
+ # @out_of_home_failover_mode are consulted instead.
46
+ @in_home_failover_mode = nil
47
+ @out_of_home_failover_mode = nil
48
+ @home_region = nil
49
+ @accessible_regions = nil
50
+ # Hosts whose region could not be determined, so that each is only logged once per failover.
51
+ @regionless_hosts = Set.new
52
+ end
53
+
54
+ private
55
+
56
+ def init_failover_mode
57
+ return unless @rds_url_type.nil?
58
+
59
+ initial_host = connection_service.initial_host_info
60
+ @rds_url_type = Utils::RdsUtils.identify_rds_type(initial_host&.host)
61
+ reject_rds_proxy_endpoint
62
+
63
+ @home_region = resolve_home_region(initial_host)
64
+ @accessible_regions = Utils::AccessibleRegions.parse(@wrapper_props)
65
+
66
+ if @accessible_regions && !@accessible_regions.include?(@home_region.downcase)
67
+ raise Errors::AwsError,
68
+ "Home region '#{@home_region}' is not included in the list of accessible regions " \
69
+ "#{@accessible_regions.to_a}. The home region must be accessible."
70
+ end
71
+
72
+ @in_home_failover_mode = resolve_failover_mode(PropertyDefinition::IN_HOME_FAILOVER_MODE)
73
+ @out_of_home_failover_mode = resolve_failover_mode(PropertyDefinition::OUT_OF_HOME_FAILOVER_MODE)
74
+
75
+ logger.debug do
76
+ "failover_home_region=#{@home_region}, accessible_regions=#{@accessible_regions&.to_a}, " \
77
+ "in_home_failover_mode=#{@in_home_failover_mode}, " \
78
+ "out_of_home_failover_mode=#{@out_of_home_failover_mode}"
79
+ end
80
+ end
81
+
82
+ # @return [String] the configured home region, or the region of the initial endpoint
83
+ # @raise [Errors::AwsError] if no home region is configured and none can be derived
84
+ def resolve_home_region(initial_host)
85
+ configured = PropertyDefinition::FAILOVER_HOME_REGION.get_string(@wrapper_props)
86
+ unless configured.nil? || configured.strip.empty?
87
+ normalized = configured.strip
88
+ unless Utils::RdsUtils.valid_region?(normalized)
89
+ raise Errors::AwsError,
90
+ "#{PropertyDefinition::FAILOVER_HOME_REGION.name} '#{configured}' is an unknown or misspelled AWS region"
91
+ end
92
+ return normalized
93
+ end
94
+
95
+ derived = @rds_url_type&.region? ? Utils::RdsUtils.rds_region(initial_host&.host) : nil
96
+ if derived.nil? || derived.empty?
97
+ raise Errors::AwsError,
98
+ "Unable to determine region from endpoint #{initial_host&.host}. If you are connecting via a global database " \
99
+ "endpoint or non-standard URL, please set the #{PropertyDefinition::FAILOVER_HOME_REGION.name} property."
100
+ end
101
+
102
+ derived
103
+ end
104
+
105
+ # Resolves one of the two configured modes, defaulting based on the initial endpoint type.
106
+ #
107
+ # @param property [WrapperProperty]
108
+ # @return [Symbol] one of the {GdbFailoverMode} constants
109
+ def resolve_failover_mode(property)
110
+ configured = GdbFailoverMode.from_value(property.get_string(@wrapper_props))
111
+ return configured unless configured.nil?
112
+
113
+ if [Utils::RdsUrlType::RDS_WRITER_CLUSTER, Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER].include?(@rds_url_type)
114
+ GdbFailoverMode::STRICT_WRITER
115
+ else
116
+ GdbFailoverMode::HOME_READER_OR_WRITER
117
+ end
118
+ end
119
+
120
+ # Whether a read-only error should trigger failover, i.e. whether strict_writer is the mode
121
+ # that currently applies.
122
+ #
123
+ # Which of the two configured modes applies depends on the region of the GDB primary, which is
124
+ # not known yet: a read-only error is a sign that the primary has changed, so the latest known
125
+ # topology may no longer say where it is. When only one of the modes is +strict_writer+ that mode
126
+ # is assumed, so that a connection that has become read-only is not left as is. {#failover}
127
+ # resolves the mode again once the new primary is known.
128
+ #
129
+ # @return [Boolean]
130
+ def failover_on_read_only_error?
131
+ in_home_strict_writer = @in_home_failover_mode == GdbFailoverMode::STRICT_WRITER
132
+ out_of_home_strict_writer = @out_of_home_failover_mode == GdbFailoverMode::STRICT_WRITER
133
+ return in_home_strict_writer if in_home_strict_writer == out_of_home_strict_writer
134
+
135
+ logger.debug do
136
+ 'A read-only error was encountered. The region of the GDB primary is not known yet, but the error suggests ' \
137
+ 'the primary may have changed, so driver failover will be triggered.'
138
+ end
139
+
140
+ true
141
+ end
142
+
143
+ # The mode that applies given the region of the current GDB primary.
144
+ #
145
+ # @param primary_region [String] the region the primary writer is in. Must not be nil - callers are
146
+ # responsible for handling endpoints whose region cannot be determined.
147
+ # @return [Symbol] one of the {GdbFailoverMode} constants
148
+ def current_failover_mode(primary_region)
149
+ home_region?(primary_region) ? @in_home_failover_mode : @out_of_home_failover_mode
150
+ end
151
+
152
+ # Whether the given region is the home region.
153
+ #
154
+ # @param region [String] must not be nil; callers are responsible for handling endpoints
155
+ # whose region cannot be determined.
156
+ # @return [Boolean]
157
+ def home_region?(region)
158
+ @home_region.casecmp?(region)
159
+ end
160
+
161
+ # Whether a host is in one of the accessible regions. All hosts are accessible when no
162
+ # region restriction is configured.
163
+ #
164
+ # @param host_info [Host::HostInfo]
165
+ # @return [Boolean]
166
+ def accessible_region?(host_info)
167
+ return true if @accessible_regions.nil?
168
+
169
+ region = Utils::RdsUtils.rds_region(host_info.host)
170
+ !region.nil? && @accessible_regions.include?(region.downcase)
171
+ end
172
+
173
+ def failover
174
+ if @closed_explicitly
175
+ logger.debug { 'Connection was explicitly closed, skipping failover' }
176
+ return
177
+ end
178
+
179
+ failover_start = Time.now
180
+ failover_deadline = failover_start + @failover_timeout
181
+ @regionless_hosts.clear
182
+
183
+ logger.info do
184
+ "Starting global database failover from #{connection_service.current_host_info&.url || 'an unknown host'}. " \
185
+ "accessible_regions=#{@accessible_regions.nil? ? 'all' : @accessible_regions.to_a}, " \
186
+ "in_home_failover_mode=#{@in_home_failover_mode}, " \
187
+ "out_of_home_failover_mode=#{@out_of_home_failover_mode}, " \
188
+ "reader_host_selector_strategy=#{@reader_selector_strategy}, " \
189
+ "failover_timeout_sec=#{@failover_timeout}"
190
+ end
191
+
192
+ # This is expected to return once the topology has stabilized, i.e. once the cluster
193
+ # control plane has already chosen a new writer.
194
+ unless host_service.force_refresh_host_list?(verify_writer: true, timeout_sec: @failover_timeout)
195
+ raise Errors::FailoverFailedError, 'The request to discover the new topology timed out or was unsuccessful'
196
+ end
197
+
198
+ writer_candidate = host_service.all_hosts.find { |h| h.role == Host::HostRole::WRITER }
199
+ if writer_candidate.nil?
200
+ raise Errors::FailoverFailedError,
201
+ "Unable to find a writer in the updated host list: #{host_service.all_hosts.map(&:url)}"
202
+ end
203
+
204
+ writer_region = Utils::RdsUtils.rds_region(writer_candidate.host)
205
+ if writer_region.nil? || writer_region.empty?
206
+ # Unable to determine whether the writer is in-home or out-of-home. The writer usually stays in the same region during failover,
207
+ # so we will assume the user connected to the home region and the writer stayed in-home.
208
+ mode = @in_home_failover_mode
209
+ else
210
+ mode = current_failover_mode(writer_region)
211
+ end
212
+
213
+ log_failover_plan(writer_candidate, writer_region, mode)
214
+ if mode == GdbFailoverMode::STRICT_WRITER
215
+ failover_to_writer(writer_candidate, writer_region, failover_deadline)
216
+ else
217
+ failover_to_allowed_host(mode, failover_deadline)
218
+ end
219
+ ensure
220
+ duration_ms = ((Time.now - failover_start) * 1000).round if failover_start
221
+ logger.debug { "Failover duration: #{duration_ms}ms" } if duration_ms
222
+ end
223
+
224
+ # Explains which mode was chosen, and what the plugin will do with it.
225
+ #
226
+ # @param writer_candidate [Host::HostInfo] the writer the topology settled on
227
+ # @param writer_region [String, nil] the region the new writer is in
228
+ # @param mode [Symbol] one of the {GdbFailoverMode} constants
229
+ def log_failover_plan(writer_candidate, writer_region, mode)
230
+ if writer_region.nil?
231
+ # Hosts in a GDB topology are built from the region-prefixed instance patterns in
232
+ # global_cluster_instance_host_patterns, so they normally always carry a parseable
233
+ # region. A writer that does not is a sign of a misconfigured pattern.
234
+ logger.warn do
235
+ "Unable to determine region of writer #{writer_candidate.host}. Please ensure you have set the " \
236
+ "#{PropertyDefinition::GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.name} setting. Failover will assume " \
237
+ "in-home failover mode #{@in_home_failover_mode}."
238
+ end
239
+
240
+ return
241
+ end
242
+
243
+ logger.info do
244
+ primary = if home_region?(writer_region)
245
+ "The GDB primary is now #{writer_candidate.url}, which is in the home region " \
246
+ "'#{@home_region}'. Using in_home_failover_mode=#{mode}."
247
+ else
248
+ "The GDB primary is now #{writer_candidate.url}, which is in region '#{writer_region}' " \
249
+ "rather than the home region '#{@home_region}'. Using out_of_home_failover_mode=#{mode}."
250
+ end
251
+
252
+ "#{primary} #{failover_target_description(mode)}"
253
+ end
254
+ end
255
+
256
+ # A plain-language description of the hosts the given mode will target.
257
+ #
258
+ # @param mode [Symbol] one of the {GdbFailoverMode} constants
259
+ # @return [String]
260
+ def failover_target_description(mode)
261
+ case mode
262
+ when GdbFailoverMode::STRICT_WRITER then 'Connecting to the new writer.'
263
+ when GdbFailoverMode::STRICT_HOME_READER then "Connecting to a reader in the home region '#{@home_region}'."
264
+ when GdbFailoverMode::STRICT_OUT_OF_HOME_READER then "Connecting to a reader outside the home region '#{@home_region}'."
265
+ when GdbFailoverMode::STRICT_ANY_READER then 'Connecting to a reader in any region.'
266
+ when GdbFailoverMode::HOME_READER_OR_WRITER then "Connecting to the writer or a reader in the home region '#{@home_region}'."
267
+ when GdbFailoverMode::OUT_OF_HOME_READER_OR_WRITER
268
+ "Connecting to the writer or a reader outside the home region '#{@home_region}'."
269
+ when GdbFailoverMode::ANY_READER_OR_WRITER then 'Connecting to the writer or a reader in any region.'
270
+ else "Connecting to a host allowed by failover mode #{mode}."
271
+ end
272
+ end
273
+
274
+ def failover_to_writer(writer_candidate, writer_region, deadline)
275
+ unless accessible_region?(writer_candidate)
276
+ raise Errors::FailoverFailedError,
277
+ "Writer is in region '#{writer_region}' which is not in the list of accessible regions " \
278
+ "#{@accessible_regions.to_a}."
279
+ end
280
+
281
+ was_in_transaction = @service_container.session_state_service.in_transaction?
282
+ result = nil
283
+ success = false
284
+
285
+ begin
286
+ result = @retry_util.connect_to_writer(self, @service_container.plugin_manager, deadline: deadline)
287
+ if result&.connection && result.host_info
288
+ success = true
289
+ connection_service.update_current_connection(result.connection, result.host_info)
290
+ raise_failover_success_error(was_in_transaction)
291
+ end
292
+ rescue Timeout::Error
293
+ raise Errors::FailoverFailedError,
294
+ "Failover timed out after #{@failover_timeout}s. " \
295
+ "Unable to connect to the new writer #{writer_candidate.host}."
296
+ ensure
297
+ close_quietly(result&.connection) unless success
298
+ end
299
+ end
300
+
301
+ def failover_to_allowed_host(mode, deadline)
302
+ was_in_transaction = @service_container.session_state_service.in_transaction?
303
+ result = nil
304
+ success = false
305
+
306
+ begin
307
+ result = @retry_util.connect_to_allowed_host(
308
+ self,
309
+ @service_container.plugin_manager,
310
+ verify_role: verify_role_for(mode),
311
+ strategy: @reader_selector_strategy,
312
+ deadline: deadline
313
+ ) { |allowed_hosts| allowed_hosts_for(mode, allowed_hosts) }
314
+ if result&.connection && result.host_info
315
+ success = true
316
+ connection_service.update_current_connection(result.connection, result.host_info)
317
+ raise_failover_success_error(was_in_transaction)
318
+ end
319
+ rescue Timeout::Error
320
+ raise Errors::FailoverFailedError,
321
+ "Failover timed out after #{@failover_timeout}s. Unable to connect to a host allowed by failover mode #{mode}."
322
+ ensure
323
+ close_quietly(result&.connection) unless success
324
+ end
325
+ end
326
+
327
+ # The role a new connection must report, or nil when either role is acceptable.
328
+ #
329
+ # @param mode [Symbol] one of the {GdbFailoverMode} constants
330
+ # @return [Symbol, nil]
331
+ def verify_role_for(mode)
332
+ case mode
333
+ when GdbFailoverMode::STRICT_HOME_READER,
334
+ GdbFailoverMode::STRICT_OUT_OF_HOME_READER,
335
+ GdbFailoverMode::STRICT_ANY_READER
336
+ Host::HostRole::READER
337
+ end
338
+ end
339
+
340
+ # The hosts that may be connected to under the given mode. Called on every retry with the
341
+ # allowed hosts from the latest topology refresh.
342
+ #
343
+ # @param mode [Symbol] one of the {GdbFailoverMode} constants
344
+ # @param allowed_hosts [Array<Host::HostInfo>] the current allowed hosts
345
+ # @return [Array<Host::HostInfo>]
346
+ def allowed_hosts_for(mode, allowed_hosts)
347
+ hosts = allowed_hosts.select { |host| host_allowed?(host, mode) }
348
+ hosts.select { |host| accessible_region?(host) }
349
+ end
350
+
351
+ def host_allowed?(host, mode)
352
+ reader = host.role == Host::HostRole::READER
353
+ writer = host.role == Host::HostRole::WRITER
354
+
355
+ case mode
356
+ when GdbFailoverMode::STRICT_HOME_READER then reader && region_position(host) == :in_home
357
+ when GdbFailoverMode::STRICT_OUT_OF_HOME_READER then reader && region_position(host) == :out_of_home
358
+ when GdbFailoverMode::STRICT_ANY_READER then reader
359
+ when GdbFailoverMode::HOME_READER_OR_WRITER then writer || (reader && region_position(host) == :in_home)
360
+ when GdbFailoverMode::OUT_OF_HOME_READER_OR_WRITER then writer || (reader && region_position(host) == :out_of_home)
361
+ when GdbFailoverMode::ANY_READER_OR_WRITER then true
362
+ else raise Errors::AwsError, "Unsupported global database failover mode: #{mode}"
363
+ end
364
+ end
365
+
366
+ # Where the given host sits relative to the home region. Only consulted for modes that place
367
+ # a region requirement on the host; +strict_any_reader+ and +any_reader_or_writer+ accept a
368
+ # host regardless of its region and so never call this.
369
+ #
370
+ # @param host [Host::HostInfo]
371
+ # @return [Symbol, nil] +:in_home+, +:out_of_home+, or nil if the region could not be determined
372
+ def region_position(host)
373
+ region = Utils::RdsUtils.rds_region(host.host)
374
+ if region.nil? || region.empty?
375
+ # This scenario is not expected: topology hosts are built from the region-prefixed instance patterns in
376
+ # global_cluster_instance_host_patterns, so they should always carry a parseable region. Without one the
377
+ # configured mode cannot be honoured for this host, so it is skipped. Only the first occurrence is logged,
378
+ # since this method is called on every failover retry and the reason for the failure does not change.
379
+ if @regionless_hosts.add?(host.host)
380
+ logger.debug do
381
+ "Unable to determine the region of #{host.host}, so it will not be considered an allowed host for the " \
382
+ 'configured failover mode.'
383
+ end
384
+ end
385
+ return nil
386
+ end
387
+
388
+ home_region?(region) ? :in_home : :out_of_home
389
+ end
390
+
391
+ # @raise [NotImplementedError] always; see {#failover} for this plugin's implementation
392
+ def failover_reader
393
+ raise NotImplementedError, "#{self.class} performs failover via #failover"
394
+ end
395
+
396
+ # @raise [NotImplementedError] always; see {#failover} for this plugin's implementation
397
+ def failover_writer
398
+ raise NotImplementedError, "#{self.class} performs failover via #failover"
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,159 @@
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 '../utils/aws_credentials_utils'
20
+ require_relative '../utils/iam_auth_utils'
21
+ require_relative '../utils/rds_utils'
22
+ require_relative '../utils/rds_url_type'
23
+ require_relative '../driver_dialects/driver_dialect_manager'
24
+ require_relative '../property_definition'
25
+
26
+ module AwsAdvancedRubyDriverWrapper
27
+ module Plugins
28
+ class IamAuthPlugin
29
+ SUBSCRIBED_METHODS = Set['connect', 'internal_connect'].freeze
30
+ IAM_TOKEN_CACHE_NAME = :iam_token
31
+
32
+ attr_reader :subscribed_methods
33
+
34
+ def initialize(service_container, props = ::Concurrent::Map.new)
35
+ ensure_aws_sdk!
36
+ @service_container = service_container
37
+ @wrapper_props = props
38
+ @credentials_provider = PropertyDefinition::AWS_CREDENTIALS_PROVIDER.get(props) ||
39
+ Aws::CredentialProviderChain.new.resolve
40
+ expiration = PropertyDefinition::IAM_EXPIRATION_SEC.get_float(props)
41
+ PropertyDefinition::IAM_EXPIRATION_SEC.validate!(expiration)
42
+ service_container.storage_service.register(IAM_TOKEN_CACHE_NAME, ttl: expiration)
43
+ @subscribed_methods = SUBSCRIBED_METHODS
44
+ end
45
+
46
+ def connect(host_info, driver_props, _is_initial_connection, pipeline_callable)
47
+ iam_connect(host_info, driver_props, pipeline_callable)
48
+ end
49
+
50
+ def internal_connect(host_info, driver_props, wrapper_props_override, _is_initial_connection, pipeline_callable)
51
+ iam_connect(host_info, driver_props, pipeline_callable, wrapper_props_override || @wrapper_props)
52
+ end
53
+
54
+ def self.clear_cache(storage_service)
55
+ storage_service.clear(IAM_TOKEN_CACHE_NAME)
56
+ end
57
+
58
+ private
59
+
60
+ def iam_connect(host_info, driver_props, pipeline_callable, wrapper_props_override = nil)
61
+ wrapper_props_override ||= @wrapper_props
62
+
63
+ user = driver_props[:user] || driver_props[:username]
64
+ raise Errors::IamAuthError, 'IamAuthPlugin: :user is required' if user.nil? || user.empty?
65
+
66
+ host = Utils::IamAuthUtils.resolve_host(
67
+ PropertyDefinition::IAM_HOST.get(wrapper_props_override), host_info
68
+ )
69
+ rds_type = Utils::RdsUtils.identify_rds_type(host)
70
+ begin
71
+ region = Utils::IamAuthUtils.region_for(
72
+ host:, props: wrapper_props_override, rds_type:, credentials_provider: @credentials_provider,
73
+ rds_client_func: -> { rds_client }
74
+ )
75
+ rescue Aws::Errors::MissingRegionError
76
+ raise Errors::IamAuthError,
77
+ 'IamAuthPlugin: unable to determine connection region. ' \
78
+ "If you are using a non-standard RDS URL, please set the 'iam_region' property."
79
+ end
80
+ unless region
81
+ raise Errors::IamAuthError,
82
+ 'IamAuthPlugin: unable to determine connection region. ' \
83
+ "If you are using a non-standard RDS URL, please set the 'iam_region' property."
84
+ end
85
+
86
+ token_prop = PropertyDefinition::IAM_ACCESS_TOKEN_PROPERTY_NAME.get(wrapper_props_override).to_sym
87
+
88
+ port = Utils::IamAuthUtils.resolve_port(
89
+ PropertyDefinition::IAM_PORT.get(wrapper_props_override),
90
+ host_info,
91
+ @service_container.dialect_service.db_dialect.default_port
92
+ )
93
+
94
+ credentials = Utils::AwsCredentialsUtils.snapshot(@credentials_provider)
95
+ cache_key = token_cache_key(region, host, port, user, credentials)
96
+ entry = cache_key && @service_container.storage_service.get(IAM_TOKEN_CACHE_NAME, cache_key)
97
+ expiration = PropertyDefinition::IAM_EXPIRATION_SEC.get_float(wrapper_props_override)
98
+ PropertyDefinition::IAM_EXPIRATION_SEC.validate!(expiration)
99
+
100
+ if Utils::IamAuthUtils.valid_entry?(entry)
101
+ driver_props[token_prop] = entry.token
102
+ is_cached_token = true
103
+ else
104
+ driver_props[token_prop] = fetch_and_cache_token(region, host, port, user, credentials, cache_key, expiration)
105
+ is_cached_token = false
106
+ end
107
+
108
+ begin
109
+ pipeline_callable.call
110
+ rescue StandardError => e
111
+ raise unless is_cached_token && @service_container.dialect_service.login_error?(e)
112
+
113
+ driver_props[token_prop] = fetch_and_cache_token(region, host, port, user, credentials, cache_key, expiration)
114
+ pipeline_callable.call
115
+ end
116
+ end
117
+
118
+ # The cache key for a token signed with the given credentials snapshot, or nil when there are
119
+ # no credentials. Without credentials there is nothing to tell one connection's token from
120
+ # another's, so such a connection neither reads nor writes the shared cache.
121
+ def token_cache_key(region, host, port, user, credentials)
122
+ return nil if credentials.nil?
123
+
124
+ Utils::IamAuthUtils.cache_key(region, host, port, user, Utils::AwsCredentialsUtils.identity(credentials))
125
+ end
126
+
127
+ # Generates a token signed with the same credentials snapshot its cache key was built from
128
+ # and, when there is a cache key, caches it. Without a snapshot the provider is passed as is,
129
+ # and signing fails for lack of credentials.
130
+ def fetch_and_cache_token(region, host, port, user, credentials, cache_key, expiration)
131
+ token = Aws::RDS::AuthTokenGenerator.new(credentials: credentials || @credentials_provider)
132
+ .auth_token(region:, endpoint: "#{host}:#{port}", user_name: user)
133
+ if cache_key
134
+ @service_container.storage_service.set(
135
+ IAM_TOKEN_CACHE_NAME, cache_key, Utils::IamAuthUtils.build_token_entry(token, expiration)
136
+ )
137
+ end
138
+ token
139
+ end
140
+
141
+ def rds_client
142
+ @rds_client ||= begin
143
+ region = PropertyDefinition::IAM_REGION.get(@wrapper_props)
144
+ Aws::RDS::Client.new(
145
+ credentials: @credentials_provider,
146
+ **(region ? { region: region } : {})
147
+ )
148
+ end
149
+ end
150
+
151
+ def ensure_aws_sdk!
152
+ require 'aws-sdk-rds'
153
+ rescue LoadError
154
+ raise LoadError,
155
+ "The IAM auth plugin requires 'aws-sdk-rds'. Add it to your Gemfile: gem 'aws-sdk-rds'"
156
+ end
157
+ end
158
+ end
159
+ end