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,485 @@
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_role'
20
+ require_relative '../host/host_availability'
21
+ require_relative '../logging'
22
+ require_relative '../property_definition'
23
+ require_relative '../utils/accessible_regions'
24
+ require_relative '../utils/rds_utils'
25
+ require_relative '../utils/rds_url_type'
26
+
27
+ module AwsAdvancedRubyDriverWrapper
28
+ module Plugins
29
+ class InitialConnectionStrategyPlugin
30
+ include Logging
31
+
32
+ SUBSCRIBED_METHODS = Set['connect'].freeze
33
+
34
+ SUBSTITUTION_STRATEGIES = {
35
+ 'writer' => :substitute_writer,
36
+ 'reader' => :substitute_reader,
37
+ 'any' => :substitute_any,
38
+ 'none' => :none
39
+ }.freeze
40
+
41
+ VERIFY_ROLES = {
42
+ 'writer' => :writer,
43
+ 'reader' => :reader,
44
+ 'none' => :none
45
+ }.freeze
46
+
47
+ attr_reader :subscribed_methods
48
+
49
+ def initialize(service_container, props = ::Concurrent::Map.new)
50
+ @service_container = service_container
51
+
52
+ @retry_timeout_sec = PropertyDefinition::INITIAL_CONNECTION_RETRY_TIMEOUT_SEC.get_float(props)
53
+ @retry_interval_sec = PropertyDefinition::INITIAL_CONNECTION_RETRY_INTERVAL_SEC.get_float(props)
54
+ @wait_for_topology_sec = PropertyDefinition::INITIAL_CONNECTION_WAIT_FOR_TOPOLOGY_SEC.get_float(props)
55
+ @host_selector_strategy = PropertyDefinition::INITIAL_CONNECTION_HOST_SELECTOR_STRATEGY.get(props)
56
+ @accessible_regions = Utils::AccessibleRegions.parse(props)
57
+ @subscribed_methods = SUBSCRIBED_METHODS
58
+
59
+ parse_role_props(props)
60
+ end
61
+
62
+ def connect(host_info, driver_props, is_initial_connection, pipeline_callable)
63
+ return pipeline_callable.call unless is_initial_connection
64
+
65
+ host = host_info&.host
66
+ url_type = Utils::RdsUtils.identify_rds_type(host)
67
+
68
+ # Wait for the cluster topology to be discovered before deciding, if the caller opted in.
69
+ wait_for_topology_if_configured if cluster_url?(url_type)
70
+
71
+ # Classify a plain writer cluster URL as single-region, global-active, global-inactive, or unresolved.
72
+ classification = url_type == Utils::RdsUrlType::RDS_WRITER_CLUSTER ? classify_writer_cluster(host) : nil
73
+ substitution_strategy = determine_substitution_strategy(url_type, classification)
74
+ role_to_verify = determine_role_to_verify(url_type, classification, substitution_strategy)
75
+
76
+ # Only cluster-type endpoints are candidates for substitution/verification. Anything else
77
+ # (instance, proxy, IP, custom domain, ...) connects directly to the provided URL.
78
+ return pipeline_callable.call unless cluster_url?(url_type)
79
+
80
+ connect_with_retry(host_info, url_type, substitution_strategy, role_to_verify, driver_props, pipeline_callable)
81
+ end
82
+
83
+ private
84
+
85
+ def connect_with_retry(host_info, url_type, substitution_strategy, role_to_verify, driver_props, pipeline_callable)
86
+ deadline = monotonic_time + @retry_timeout_sec
87
+ conn = nil
88
+ success = false
89
+
90
+ begin
91
+ while monotonic_time < deadline
92
+ candidate_host = resolve_candidate_host(host_info, url_type, substitution_strategy)
93
+
94
+ begin
95
+ conn = open_connection_to(
96
+ candidate_host, host_info, substitution_strategy, driver_props, pipeline_callable
97
+ )
98
+
99
+ if conn.nil?
100
+ sleep(@retry_interval_sec)
101
+ next
102
+ end
103
+
104
+ if role_to_verify.nil?
105
+ success = true
106
+ return conn
107
+ end
108
+
109
+ conn_role = dialect_service.db_dialect.host_role(conn)
110
+ if conn_role == role_to_verify
111
+ success = true
112
+ return conn
113
+ end
114
+
115
+ host_service.force_refresh_host_list?
116
+ if role_to_verify == Host::HostRole::READER && !readers_in_topology?(host_service.all_hosts)
117
+ logger.warn('Reader verification expected but no readers exist in topology; accepting connection with writer role')
118
+ success = true
119
+ return conn
120
+ end
121
+
122
+ logger.debug("Connection to #{candidate_host&.host} has role #{conn_role}, expected #{role_to_verify}; retrying")
123
+ close_connection(conn)
124
+ conn = nil
125
+ sleep(@retry_interval_sec)
126
+ rescue StandardError => e
127
+ close_connection(conn)
128
+ conn = nil
129
+
130
+ raise if dialect_service.login_error?(e)
131
+
132
+ if dialect_service.network_error?(e)
133
+ host_service.set_availability(candidate_host, Host::HostAvailability::UNAVAILABLE) if candidate_host
134
+ # Back off before retrying, as the other retry paths do. Without this, a persistently
135
+ # unreachable topology (e.g. every instance down) busy-loops to the deadline, spinning the
136
+ # CPU and flooding the logs.
137
+ sleep(@retry_interval_sec)
138
+ next
139
+ end
140
+
141
+ if dialect_service.read_only_error?(e) && substitution_strategy == :substitute_writer
142
+ sleep(@retry_interval_sec)
143
+ next
144
+ end
145
+
146
+ raise
147
+ end
148
+ end
149
+ ensure
150
+ close_connection(conn) unless success
151
+ end
152
+
153
+ raise Errors::AwsError,
154
+ "Initial connection strategy timed out after #{@retry_timeout_sec}s. " \
155
+ "Substitution: #{substitution_strategy}, verification: #{role_to_verify}"
156
+ end
157
+
158
+ def resolve_candidate_host(original_host_info, url_type, substitution_strategy)
159
+ return original_host_info if substitution_strategy == :none
160
+
161
+ candidate = select_candidate_host(original_host_info, url_type, substitution_strategy)
162
+ return candidate if candidate && Utils::RdsUtils.rds_instance?(candidate.host)
163
+
164
+ # No instance URL available to substitute. This happens when topology hasn't been successfully queried yet.
165
+ # Fall back to connecting via the initial endpoint.
166
+ # Callers that want to wait for topology first opt in via INITIAL_CONNECTION_WAIT_FOR_TOPOLOGY_SEC (handled in #connect).
167
+ logger.debug("Unable to resolve a substitute instance host for strategy '#{substitution_strategy}'; \
168
+ connecting via the original endpoint '#{original_host_info&.host}'")
169
+ original_host_info
170
+ end
171
+
172
+ # When INITIAL_CONNECTION_WAIT_FOR_TOPOLOGY_SEC is positive and only the initial (non-instance)
173
+ # endpoint is known, block up to the timeout for the topology monitor to discover instance URLs
174
+ # before making substitution/verification decisions. Limitation: force_refresh_host_list returns
175
+ # the initial host list when the dialect is not final, so topology may still be unavailable after waiting.
176
+ def wait_for_topology_if_configured
177
+ return unless @wait_for_topology_sec.positive?
178
+ return unless only_initial_endpoint_known?
179
+
180
+ host_service.force_refresh_host_list?(timeout_sec: @wait_for_topology_sec)
181
+ end
182
+
183
+ # True when the topology contains a single host that is not an instance URL, i.e. we only have
184
+ # the initial connection endpoint and the topology has not been queried yet.
185
+ def only_initial_endpoint_known?
186
+ hosts = host_service.all_hosts
187
+ hosts.size <= 1 && hosts.none? { |h| Utils::RdsUtils.rds_instance?(h.host) }
188
+ end
189
+
190
+ # True once real topology has been discovered: more than one host, or a single instance URL.
191
+ def topology_available?
192
+ hosts = host_service.all_hosts
193
+ return false if hosts.empty?
194
+
195
+ hosts.size > 1 || Utils::RdsUtils.rds_instance?(hosts.first.host)
196
+ end
197
+
198
+ def open_connection_to(candidate_host, original_host_info, substitution_strategy, driver_props, pipeline_callable)
199
+ if substitution_strategy == :none || candidate_host == original_host_info
200
+ conn = pipeline_callable.call
201
+ # Refresh topology in background when connecting via cluster endpoint
202
+ host_service.force_refresh_host_list? if substitution_strategy != :none
203
+ return conn
204
+ end
205
+
206
+ plugin_manager.connect(candidate_host, driver_props, true, plugin_to_skip: self)
207
+ end
208
+
209
+ # A writer cluster URL falls into one of these buckets, decided by the dialect first
210
+ # (authoritative) and topology second (only to split active vs inactive within a global cluster):
211
+ # :single_region - dialect is final and not global -> connected to the writer's own cluster
212
+ # :global_active - global dialect, topology shows the writer is in this endpoint's region
213
+ # :global_inactive - global dialect, topology shows the writer is in another region
214
+ # :unresolved - dialect not final, or global but topology can't tell us which cluster this is
215
+ def classify_writer_cluster(host)
216
+ return :unresolved unless dialect_service.dialect_final?
217
+ return :single_region unless dialect_service.db_dialect.global?
218
+
219
+ # Global cluster: use the confirmed cross-region topology to locate the writer.
220
+ return :unresolved unless topology_available?
221
+
222
+ writer = find_writer_in_topology
223
+ return :unresolved if writer.nil? || !Utils::RdsUtils.rds_instance?(writer.host)
224
+
225
+ Utils::RdsUtils.same_region?(writer.host, host) ? :global_active : :global_inactive
226
+ end
227
+
228
+ def determine_substitution_strategy(url_type, classification)
229
+ # @explicit_substitution was parsed at init; only the URL-dependent validity is checked here.
230
+ if @explicit_substitution
231
+ validate_substitution_strategy(@explicit_substitution, url_type)
232
+ return @explicit_substitution
233
+ end
234
+
235
+ case url_type
236
+ when Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
237
+ :substitute_writer
238
+ when Utils::RdsUrlType::RDS_WRITER_CLUSTER
239
+ writer_cluster_substitution(classification)
240
+ when Utils::RdsUrlType::RDS_READER_CLUSTER
241
+ :substitute_reader
242
+ else
243
+ :none
244
+ end
245
+ end
246
+
247
+ def determine_role_to_verify(url_type, classification, substitution_strategy)
248
+ role = resolve_verify_role(url_type, classification, substitution_strategy)
249
+
250
+ # :none is the explicit "skip verification" sentinel; normalize it to nil, which
251
+ # connect_with_retry treats as "no role to verify".
252
+ role == :none ? nil : role
253
+ end
254
+
255
+ def resolve_verify_role(url_type, classification, substitution_strategy)
256
+ # @explicit_verify_role was parsed at init; only the URL-dependent validity is checked here.
257
+ if @explicit_verify_role
258
+ validate_verify_role(@explicit_verify_role, url_type)
259
+ return @explicit_verify_role
260
+ end
261
+
262
+ case url_type
263
+ when Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
264
+ Host::HostRole::WRITER
265
+ when Utils::RdsUrlType::RDS_WRITER_CLUSTER
266
+ writer_cluster_verification(classification, substitution_strategy)
267
+ when Utils::RdsUrlType::RDS_READER_CLUSTER
268
+ Host::HostRole::READER
269
+ end
270
+ end
271
+
272
+ def writer_cluster_substitution(classification)
273
+ case classification
274
+ when :single_region, :global_active
275
+ :substitute_writer
276
+ when :global_inactive
277
+ # INITIAL_CONNECTION_INACTIVE_SUBSTITUTE_HOST governs inactive cluster endpoints. When unset,
278
+ # pass the endpoint through untouched. Users who want cross-region writer substitution must opt in explicitly.
279
+ @inactive_substitution || :none
280
+ else
281
+ # :unresolved - we don't know enough to substitute safely; connect via the original endpoint.
282
+ :none
283
+ end
284
+ end
285
+
286
+ def writer_cluster_verification(classification, substitution_strategy)
287
+ case classification
288
+ when :single_region, :global_active
289
+ Host::HostRole::WRITER
290
+ when :global_inactive
291
+ # INITIAL_CONNECTION_INACTIVE_VERIFY_ROLE takes priority when set. When unset, verify writer only if we substituted
292
+ # a writer, which only happens when the user opted into substitution explicitly via INITIAL_CONNECTION_SUBSTITUTE_HOST
293
+ # or INITIAL_CONNECTION_INACTIVE_SUBSTITUTE_HOST. Otherwise, do not verify role.
294
+ if @inactive_verify_role
295
+ @inactive_verify_role
296
+ elsif substitution_strategy == :substitute_writer
297
+ Host::HostRole::WRITER
298
+ end
299
+ end
300
+ # :unresolved -> nil (no verification)
301
+ end
302
+
303
+ def select_candidate_host(original_host_info, url_type, substitution_strategy)
304
+ return original_host_info if substitution_strategy == :none
305
+
306
+ all_hosts = host_service.all_hosts
307
+ return nil if all_hosts.empty?
308
+
309
+ if substitution_strategy == :substitute_writer
310
+ filtered = Utils::AccessibleRegions.filter_by_region(all_hosts, @accessible_regions)
311
+ return filtered.find { |h| h.role == Host::HostRole::WRITER }
312
+ end
313
+
314
+ target_role = substitution_strategy == :substitute_reader ? Host::HostRole::READER : nil
315
+
316
+ available_hosts = Utils::AccessibleRegions.filter_by_region(host_service.hosts, @accessible_regions)
317
+
318
+ endpoint_region = url_type_has_region?(url_type) ? Utils::RdsUtils.rds_region(original_host_info.host) : nil
319
+ if endpoint_region
320
+ available_hosts = available_hosts.select do |h|
321
+ Utils::RdsUtils.rds_region(h.host)&.casecmp(endpoint_region)&.zero?
322
+ end
323
+ end
324
+
325
+ host_service.select_host(available_hosts, target_role, @host_selector_strategy)
326
+ rescue StandardError
327
+ nil
328
+ end
329
+
330
+ def find_writer_in_topology
331
+ host_service.all_hosts.find { |h| h.role == Host::HostRole::WRITER }
332
+ end
333
+
334
+ # Cluster-type endpoints are the only ones eligible for substitution/verification.
335
+ def cluster_url?(url_type)
336
+ [
337
+ Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER,
338
+ Utils::RdsUrlType::RDS_WRITER_CLUSTER,
339
+ Utils::RdsUrlType::RDS_READER_CLUSTER,
340
+ Utils::RdsUrlType::RDS_CUSTOM_CLUSTER
341
+ ].include?(url_type)
342
+ end
343
+
344
+ # Parse the substitution/verification props up front so a malformed value fails fast at wrapper construction.
345
+ # URL-dependent validity (e.g. 'writer' on a reader cluster) will checked in #connect.
346
+ # An unset prop parses to nil, an explicit 'none' parses to :none.
347
+ def parse_role_props(props)
348
+ raw_substitution = PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.get(props)
349
+ @explicit_substitution = raw_substitution && parse_substitution_value(raw_substitution)
350
+
351
+ raw_verify_role = PropertyDefinition::INITIAL_CONNECTION_VERIFY_ROLE.get(props)
352
+ @explicit_verify_role = raw_verify_role && parse_verify_role_value(raw_verify_role)
353
+
354
+ raw_inactive_substitution = PropertyDefinition::INITIAL_CONNECTION_INACTIVE_SUBSTITUTE_HOST.get(props)
355
+ @inactive_substitution = raw_inactive_substitution && parse_inactive_substitution_value(raw_inactive_substitution)
356
+
357
+ raw_inactive_verify_role = PropertyDefinition::INITIAL_CONNECTION_INACTIVE_VERIFY_ROLE.get(props)
358
+ @inactive_verify_role = raw_inactive_verify_role && parse_inactive_verify_role_value(raw_inactive_verify_role)
359
+ end
360
+
361
+ def parse_inactive_substitution_value(value)
362
+ strategy = parse_substitution_value(value)
363
+ return strategy if %i[substitute_writer none].include?(strategy)
364
+
365
+ raise Errors::AwsError,
366
+ "#{PropertyDefinition::INITIAL_CONNECTION_INACTIVE_SUBSTITUTE_HOST.name}: '#{value}' is not valid. " \
367
+ "Valid values are 'writer' or 'none'."
368
+ end
369
+
370
+ def parse_inactive_verify_role_value(value)
371
+ role = parse_verify_role_value(value)
372
+ return role if [Host::HostRole::WRITER, :none].include?(role)
373
+
374
+ raise Errors::AwsError,
375
+ "#{PropertyDefinition::INITIAL_CONNECTION_INACTIVE_VERIFY_ROLE.name}: '#{value}' is not valid. " \
376
+ "Valid values are 'writer' or 'none'."
377
+ end
378
+
379
+ def parse_substitution_value(value)
380
+ normalized = value.to_s.downcase
381
+ strategy = SUBSTITUTION_STRATEGIES[normalized]
382
+ unless strategy
383
+ raise Errors::AwsError,
384
+ "Invalid #{PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.name} value: '#{value}'. " \
385
+ "Valid values: #{SUBSTITUTION_STRATEGIES.keys.join(', ')}"
386
+ end
387
+ strategy
388
+ end
389
+
390
+ def parse_verify_role_value(value)
391
+ normalized = value.to_s.downcase
392
+ unless VERIFY_ROLES.key?(normalized)
393
+ raise Errors::AwsError,
394
+ "Invalid #{PropertyDefinition::INITIAL_CONNECTION_VERIFY_ROLE.name} value: '#{value}'. " \
395
+ "Valid values: #{VERIFY_ROLES.keys.join(', ')}"
396
+ end
397
+ VERIFY_ROLES[normalized]
398
+ end
399
+
400
+ def validate_substitution_strategy(strategy, url_type)
401
+ return if strategy == :none
402
+
403
+ if url_type == Utils::RdsUrlType::RDS_INSTANCE
404
+ raise Errors::AwsError,
405
+ "#{PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.name} cannot be set when connecting to an instance endpoint"
406
+ end
407
+
408
+ if strategy == :substitute_writer &&
409
+ [Utils::RdsUrlType::RDS_READER_CLUSTER, Utils::RdsUrlType::RDS_CUSTOM_CLUSTER].include?(url_type)
410
+ raise Errors::AwsError,
411
+ "#{PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.name}: 'writer' is invalid for reader or custom cluster endpoints"
412
+ end
413
+
414
+ if strategy == :substitute_reader &&
415
+ [Utils::RdsUrlType::RDS_WRITER_CLUSTER, Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER].include?(url_type)
416
+ raise Errors::AwsError,
417
+ "#{PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.name}: 'reader' is invalid for writer or global cluster endpoints"
418
+ end
419
+
420
+ return unless strategy == :substitute_any && url_type != Utils::RdsUrlType::RDS_CUSTOM_CLUSTER
421
+
422
+ raise Errors::AwsError,
423
+ "#{PropertyDefinition::INITIAL_CONNECTION_SUBSTITUTE_HOST.name}: 'any' is only valid for custom cluster endpoints"
424
+ end
425
+
426
+ def validate_verify_role(role, url_type)
427
+ return if role.nil? || role == :none
428
+
429
+ if role == Host::HostRole::READER &&
430
+ [Utils::RdsUrlType::RDS_WRITER_CLUSTER, Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER].include?(url_type)
431
+ raise Errors::AwsError,
432
+ "#{PropertyDefinition::INITIAL_CONNECTION_VERIFY_ROLE.name}: 'reader' is invalid for writer or global cluster endpoints"
433
+ end
434
+
435
+ # A custom endpoint can only be of type 'reader' or 'any', so writer verification is not allowed.
436
+ return unless role == Host::HostRole::WRITER &&
437
+ [Utils::RdsUrlType::RDS_READER_CLUSTER, Utils::RdsUrlType::RDS_CUSTOM_CLUSTER].include?(url_type)
438
+
439
+ raise Errors::AwsError,
440
+ "#{PropertyDefinition::INITIAL_CONNECTION_VERIFY_ROLE.name}: 'writer' is invalid for reader or custom cluster endpoints"
441
+ end
442
+
443
+ def readers_in_topology?(hosts)
444
+ return false if hosts.nil? || hosts.empty?
445
+
446
+ hosts.any? { |h| h.role == Host::HostRole::READER }
447
+ end
448
+
449
+ def url_type_has_region?(url_type)
450
+ [
451
+ Utils::RdsUrlType::RDS_WRITER_CLUSTER,
452
+ Utils::RdsUrlType::RDS_READER_CLUSTER,
453
+ Utils::RdsUrlType::RDS_CUSTOM_CLUSTER,
454
+ Utils::RdsUrlType::RDS_INSTANCE,
455
+ Utils::RdsUrlType::RDS_PROXY,
456
+ Utils::RdsUrlType::RDS_PROXY_ENDPOINT
457
+ ].include?(url_type)
458
+ end
459
+
460
+ def close_connection(conn)
461
+ return if conn.nil?
462
+
463
+ dialect_service.driver_dialect.close_connection(conn)
464
+ rescue StandardError
465
+ # Ignore errors when closing a connection during cleanup
466
+ end
467
+
468
+ def monotonic_time
469
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
470
+ end
471
+
472
+ def host_service
473
+ @service_container.host_service
474
+ end
475
+
476
+ def dialect_service
477
+ @service_container.dialect_service
478
+ end
479
+
480
+ def plugin_manager
481
+ @service_container.plugin_manager
482
+ end
483
+ end
484
+ end
485
+ end
@@ -0,0 +1,157 @@
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 'time'
18
+ require_relative '../../logging'
19
+ require_relative 'sanitizer'
20
+
21
+ module AwsAdvancedRubyDriverWrapper
22
+ module Plugins
23
+ module Encryption
24
+ # Emits one audit record per key management, kms_encryption, decryption, and metadata
25
+ # operation, enabled with the +encryption_audit_logging_enabled+ property.
26
+ #
27
+ # Records are written to the wrapper's logger as a single +AUDIT+ line of +key=value+
28
+ # fields: successful operations at info level, failures at warn level. Key ids, ARNs,
29
+ # identifiers, and error messages are redacted by {Sanitizer} before being written, so
30
+ # the audit trail never contains key material, credentials, or column values.
31
+ class AuditLogger
32
+ include Logging
33
+
34
+ AUDIT_PREFIX = 'AUDIT'
35
+
36
+ # @param enabled [Boolean] when false every method is a no-op
37
+ def initialize(enabled)
38
+ @enabled = enabled
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def enabled?
43
+ @enabled
44
+ end
45
+
46
+ # @param master_key_arn [String, nil]
47
+ # @param description [String, nil]
48
+ # @param success [Boolean]
49
+ # @param error_message [String, nil]
50
+ # @return [void]
51
+ def log_key_creation(master_key_arn:, description: nil, success: true, error_message: nil)
52
+ record('KEY_CREATION', success, error_message,
53
+ master_key_arn: Sanitizer.arn(master_key_arn),
54
+ description: Sanitizer.description(description))
55
+ end
56
+
57
+ # @return [void]
58
+ def log_data_key_generation(master_key_arn:, key_id: nil, success: true, error_message: nil)
59
+ record('DATA_KEY_GENERATION', success, error_message,
60
+ master_key_arn: Sanitizer.arn(master_key_arn),
61
+ key_id: Sanitizer.key_id(key_id))
62
+ end
63
+
64
+ # @return [void]
65
+ def log_data_key_decryption(master_key_arn:, key_id: nil, success: true, error_message: nil)
66
+ record('DATA_KEY_DECRYPTION', success, error_message,
67
+ master_key_arn: Sanitizer.arn(master_key_arn),
68
+ key_id: Sanitizer.key_id(key_id))
69
+ end
70
+
71
+ # @return [void]
72
+ def log_encryption(table_name:, column_name:, key_id: nil, success: true, error_message: nil)
73
+ record('ENCRYPTION', success, error_message,
74
+ table: Sanitizer.table_name(table_name),
75
+ column: Sanitizer.column_name(column_name),
76
+ key_id: Sanitizer.key_id(key_id))
77
+ end
78
+
79
+ # @return [void]
80
+ def log_decryption(table_name:, column_name:, key_id: nil, success: true, error_message: nil)
81
+ record('DECRYPTION', success, error_message,
82
+ table: Sanitizer.table_name(table_name),
83
+ column: Sanitizer.column_name(column_name),
84
+ key_id: Sanitizer.key_id(key_id))
85
+ end
86
+
87
+ # @param operation [String] the metadata operation, e.g. 'load' or 'refresh'
88
+ # @return [void]
89
+ def log_metadata_operation(operation:, table_name: nil, column_name: nil, success: true, error_message: nil)
90
+ record("METADATA_#{operation.to_s.upcase}", success, error_message,
91
+ table: Sanitizer.table_name(table_name),
92
+ column: Sanitizer.column_name(column_name))
93
+ end
94
+
95
+ # @return [void]
96
+ def log_configuration_change(config_type:, details: nil, success: true, error_message: nil)
97
+ record('CONFIGURATION_CHANGE', success, error_message,
98
+ config_type: Sanitizer.truncate(config_type, Sanitizer::MAX_NAME_LENGTH),
99
+ details: Sanitizer.config_details(details))
100
+ end
101
+
102
+ # @return [void]
103
+ def log_connection_parameter_extraction(strategy:, connection_type:, success: true, error_message: nil)
104
+ record('CONNECTION_PARAMETER_EXTRACTION', success, error_message,
105
+ strategy: Sanitizer.truncate(strategy, Sanitizer::MAX_NAME_LENGTH),
106
+ connection_type: Sanitizer.truncate(connection_type, Sanitizer::MAX_NAME_LENGTH))
107
+ end
108
+
109
+ # @param active [Boolean] whether connection sharing is currently in effect
110
+ # @return [void]
111
+ def log_connection_sharing_fallback(reason:, original_failure: nil, active: false)
112
+ return unless @enabled
113
+
114
+ # The fallback deactivating (returning to normal) is worth an info record; while it is
115
+ # active the steady state is logged at debug.
116
+ emit(active ? :debug : :info, 'CONNECTION_SHARING_FALLBACK', true, nil,
117
+ reason: Sanitizer.description(reason),
118
+ original_failure: Sanitizer.error_message(original_failure),
119
+ active: active)
120
+ end
121
+
122
+ # @return [void]
123
+ def log_connection_health_check(connection_type:, healthy:, success_count:, failure_count:, success_rate:)
124
+ return unless @enabled
125
+
126
+ emit(healthy ? :info : :warn, 'CONNECTION_HEALTH_CHECK', healthy, nil,
127
+ connection_type: Sanitizer.truncate(connection_type, Sanitizer::MAX_NAME_LENGTH),
128
+ healthy: healthy,
129
+ successful: success_count,
130
+ failed: failure_count,
131
+ success_rate: format('%.2f%%', success_rate * 100))
132
+ end
133
+
134
+ private
135
+
136
+ def record(operation, success, error_message, fields)
137
+ return unless @enabled
138
+
139
+ emit(success ? :info : :warn, operation, success, error_message, fields)
140
+ end
141
+
142
+ def emit(level, operation, success, error_message, fields)
143
+ logger.public_send(level, build_line(operation, success, error_message, fields))
144
+ end
145
+
146
+ def build_line(operation, success, error_message, fields)
147
+ entries = { operation: operation, success: success }
148
+ fields.each { |key, value| entries[key] = value unless value.nil? }
149
+ entries[:error] = Sanitizer.error_message(error_message) if error_message
150
+ entries[:timestamp] = Time.now.utc.iso8601
151
+
152
+ "#{AUDIT_PREFIX} #{entries.map { |key, value| "#{key}=#{value}" }.join(' ')}"
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end