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,564 @@
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 'resolv'
19
+ require_relative '../../logging'
20
+ require_relative '../../monitoring/monitor'
21
+ require_relative '../../monitoring/monitor_connection'
22
+ require_relative '../../property_definition'
23
+ require_relative '../../utils/rds_utils'
24
+ require_relative '../../utils/connection_config'
25
+ require_relative '../../services/connection_service'
26
+ require_relative '../../services/service_container'
27
+ require_relative '../../host/rds_host_list_provider'
28
+ require_relative 'phase'
29
+ require_relative 'role'
30
+ require_relative 'status_info'
31
+ require_relative 'interim_status'
32
+ require_relative 'interval_rate'
33
+
34
+ module AwsAdvancedRubyDriverWrapper
35
+ module Plugins
36
+ module BlueGreen
37
+ # Background thread that monitors the Blue/Green status table for one role (SOURCE or TARGET).
38
+ class StatusMonitor < Monitoring::Monitor
39
+ include Logging
40
+
41
+ TERMINATION_TIMEOUT_SEC = 30.0
42
+ BG_CLUSTER = PropertyDefinition::BG_STORAGE_NAMESPACE
43
+ DEFAULT_CHECK_INTERVAL_MS = 100
44
+ LATEST_KNOWN_VERSION = '1.0'
45
+ KNOWN_VERSIONS = Set[LATEST_KNOWN_VERSION].freeze
46
+
47
+ def initialize(
48
+ role,
49
+ bgd_id,
50
+ initial_host,
51
+ service_container,
52
+ monitoring_driver_props:,
53
+ status_check_interval_map:,
54
+ on_change_func:,
55
+ monitoring_wrapper_props: {}
56
+ )
57
+ super(termination_timeout_sec: TERMINATION_TIMEOUT_SEC)
58
+
59
+ @role = role
60
+ @bgd_id = bgd_id.freeze
61
+ @initial_host = initial_host.deep_dup.freeze
62
+ @service_container = service_container
63
+ @status_monitor_driver_props = monitoring_driver_props
64
+ @status_monitor_wrapper_props = monitoring_wrapper_props
65
+ @status_check_interval_map = status_check_interval_map
66
+ @on_change_func = on_change_func
67
+
68
+ @current_phase = Phase::NOT_CREATED
69
+ @version = LATEST_KNOWN_VERSION
70
+ @port = -1
71
+ @connection = Monitoring::MonitorConnection.new(service_container.dialect_service.driver_dialect)
72
+ @connection_mutex = Mutex.new
73
+ @use_ip_address = Concurrent::AtomicBoolean.new(false)
74
+ @panic_mode = Concurrent::AtomicBoolean.new(true)
75
+ @correct_connection_host = Concurrent::AtomicBoolean.new(false)
76
+ @connected_ip_address = Concurrent::AtomicReference.new(nil)
77
+ @start_ip_addresses_by_host_map = Concurrent::Hash.new
78
+ @connection_host_info = Concurrent::AtomicReference.new(nil)
79
+ @open_connection_future = Concurrent::AtomicReference.new(nil)
80
+ @event = Concurrent::Event.new
81
+ @interval_rate = Concurrent::AtomicReference.new(IntervalRate::BASELINE)
82
+ @current_topology = Concurrent::AtomicReference.new(nil)
83
+ @start_topology = nil
84
+ @collect_topology = Concurrent::AtomicBoolean.new(true)
85
+ @collect_ip_addresses = Concurrent::AtomicBoolean.new(true)
86
+ @host_names = Set.new
87
+ @current_ip_addresses_by_host_map = Concurrent::Hash.new
88
+ @all_start_topology_ip_changed = false
89
+ @all_start_topology_endpoints_removed = false
90
+ @all_topology_changed = false
91
+ end
92
+
93
+ def monitor
94
+ loop do
95
+ break if stopped?
96
+
97
+ begin
98
+ old_phase = @current_phase
99
+ open_connection
100
+ collect_status
101
+ collect_topology
102
+ collect_host_ip_addresses
103
+ update_ip_address_flags
104
+
105
+ logger.debug { "[#{@role}] Status changed to: #{@current_phase}" } if @current_phase && @current_phase != old_phase
106
+
107
+ @on_change_func&.call(
108
+ @role,
109
+ InterimStatus.new(
110
+ @current_phase,
111
+ @version,
112
+ @port,
113
+ @start_topology&.dup,
114
+ @current_topology.get&.dup,
115
+ @start_ip_addresses_by_host_map.dup,
116
+ @current_ip_addresses_by_host_map.dup,
117
+ @host_names.dup,
118
+ @all_start_topology_ip_changed,
119
+ @all_start_topology_endpoints_removed,
120
+ @all_topology_changed
121
+ )
122
+ )
123
+
124
+ delay_ms = @status_check_interval_map.fetch(
125
+ @panic_mode.true? && @current_phase != Phase::NOT_CREATED && !@current_phase.nil? ? IntervalRate::HIGH : @interval_rate.get,
126
+ DEFAULT_CHECK_INTERVAL_MS
127
+ )
128
+ delay(delay_ms)
129
+ rescue Interrupt => e
130
+ logger.debug { "[#{@role}] Interrupted." }
131
+ Thread.current.raise(e)
132
+ rescue StandardError => e
133
+ logger.warn { "[#{@role}] Unhandled exception while monitoring blue/green status: #{e.class}: #{e.message}" }
134
+ end
135
+ end
136
+ ensure
137
+ @connection_mutex.synchronize { @connection.close }
138
+ @host_list_provider&.stop_monitor if @host_list_provider.respond_to?(:stop_monitor)
139
+ @host_list_provider = nil
140
+ @open_connection_future.set(nil)
141
+ logger.debug { "[#{@role}] Blue/green status monitoring thread is completed." }
142
+ end
143
+
144
+ def open_connection
145
+ connection = @connection.get
146
+ return unless connection.nil? || connection_closed?(connection)
147
+
148
+ future = @open_connection_future.get
149
+ if future
150
+ if future.resolved?
151
+ return if @panic_mode.false?
152
+ elsif future.pending?
153
+ return
154
+ end
155
+ # future is rejected — fall through to replace it
156
+ end
157
+
158
+ new_future = Concurrent::Promises.future { attempt_open_connection }
159
+ @open_connection_future.compare_and_set(future, new_future)
160
+ end
161
+
162
+ def resolve_ip_address(host)
163
+ Resolv.getaddress(host)
164
+ rescue Resolv::ResolvError
165
+ nil
166
+ end
167
+
168
+ # Attempts to connect to all known instance IPs in parallel and returns the first
169
+ # successful connection.
170
+ def try_connect_on_all_instances(host_info)
171
+ prev_ip = @connected_ip_address.get
172
+ candidate_ips = @start_ip_addresses_by_host_map.values.compact.to_set
173
+ candidate_ips.add(prev_ip) if prev_ip
174
+
175
+ return nil if candidate_ips.empty?
176
+
177
+ logger.debug { "[#{@role}] Opening monitoring connection (IP) to #{candidate_ips.to_a.join(', ')}." }
178
+ connect_to_first_available_ip(candidate_ips, host_info)
179
+ end
180
+
181
+ def connect_to_first_available_ip(candidate_ips, host_info)
182
+ winner = Concurrent::AtomicReference.new(nil)
183
+
184
+ futures = candidate_ips.map do |ip|
185
+ Concurrent::Promises.future do
186
+ conn = try_connect_single_ip_address(ip, host_info)
187
+ if conn && !winner.compare_and_set(nil, conn)
188
+ begin
189
+ @service_container.dialect_service.driver_dialect.close_connection(conn)
190
+ rescue StandardError
191
+ nil
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ Concurrent::Promises.zip(*futures).wait(TERMINATION_TIMEOUT_SEC)
198
+ winner.get
199
+ end
200
+
201
+ def try_connect_single_ip_address(ip_address, host_info)
202
+ original_host = host_info.host
203
+ host_info = host_info.deep_dup(host: ip_address)
204
+
205
+ connect_props = @status_monitor_driver_props.dup
206
+ override_props = iam_enabled? ? map_merge(@status_monitor_wrapper_props, PropertyDefinition::IAM_HOST.name => original_host)
207
+ : @status_monitor_wrapper_props
208
+
209
+ logger.debug { "[#{@role}] Opening monitoring connection (IP) to #{ip_address}." }
210
+
211
+ connection = @service_container.plugin_manager.internal_connect(
212
+ host_info, connect_props, override_props, false
213
+ )
214
+ @connected_ip_address.set(ip_address)
215
+ connection
216
+ rescue StandardError
217
+ nil
218
+ end
219
+
220
+ def delay(delay_ms)
221
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + delay_ms
222
+ current_interval_rate = @interval_rate.get
223
+ current_panic = @panic_mode.true?
224
+ min_delay = [delay_ms, 50].min / 1000.0
225
+
226
+ loop do
227
+ @event.wait(min_delay)
228
+ break if @interval_rate.get != current_interval_rate ||
229
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) >= end_time ||
230
+ @stop_flag.true? ||
231
+ current_panic != @panic_mode.true?
232
+ end
233
+ end
234
+
235
+ def update_ip_address_flags
236
+ if @collect_topology.true?
237
+ @all_start_topology_ip_changed = false
238
+ @all_start_topology_endpoints_removed = false
239
+ @all_topology_changed = false
240
+ return
241
+ end
242
+
243
+ unless @collect_ip_addresses.true?
244
+ @all_start_topology_ip_changed = @start_topology&.any? &&
245
+ @start_topology.all? do |x|
246
+ start_ip = @start_ip_addresses_by_host_map[x.host]
247
+ current_ip = @current_ip_addresses_by_host_map[x.host]
248
+ start_ip && current_ip && start_ip != current_ip
249
+ end
250
+ end
251
+
252
+ @all_start_topology_endpoints_removed = @start_topology&.any? &&
253
+ @start_topology.all? do |x|
254
+ start_ip = @start_ip_addresses_by_host_map[x.host]
255
+ current_ip = @current_ip_addresses_by_host_map[x.host]
256
+ start_ip && current_ip.nil?
257
+ end
258
+
259
+ start_hosts = @start_topology&.to_set(&:host) || Set.new
260
+ current_topology = @current_topology.get
261
+ @all_topology_changed = current_topology&.any? &&
262
+ start_hosts.any? &&
263
+ current_topology.none? { |x| start_hosts.include?(x.host) }
264
+ end
265
+
266
+ def collect_host_ip_addresses
267
+ @current_ip_addresses_by_host_map.clear
268
+
269
+ @host_names.each do |host|
270
+ @current_ip_addresses_by_host_map[host] = resolve_ip_address(host)
271
+ end
272
+
273
+ @start_ip_addresses_by_host_map.replace(@current_ip_addresses_by_host_map) if @collect_ip_addresses.true?
274
+ end
275
+
276
+ def collect_topology
277
+ return if @host_list_provider.nil?
278
+
279
+ @connection_mutex.synchronize do
280
+ conn = @connection.get
281
+ return if conn.nil? || connection_closed?(conn)
282
+ end
283
+
284
+ topology = @host_list_provider.force_refresh(false, Host::RdsHostListProvider::DEFAULT_TOPOLOGY_QUERY_TIMEOUT_SEC)
285
+ return logger.debug { 'Timed out while waiting for force_refresh to return new topology info.' } if topology.nil?
286
+
287
+ @current_topology.set(topology)
288
+
289
+ return unless @collect_topology.true?
290
+
291
+ @start_topology = topology
292
+ @host_names.merge(topology.map(&:host))
293
+ end
294
+
295
+ def collect_status
296
+ @connection_mutex.synchronize do
297
+ conn = @connection.get
298
+ return if conn.nil? || connection_closed?(conn)
299
+
300
+ return unless status_available?(conn)
301
+
302
+ dialect = @service_container.dialect_service.db_dialect
303
+ status_entries = parse_status_entries(conn, dialect)
304
+ status_info = resolve_status_info(status_entries)
305
+ apply_status_info(status_info, status_entries)
306
+ verify_connection_host(status_info)
307
+ init_host_list_provider if @correct_connection_host.true? && @host_list_provider.nil?
308
+ end
309
+ rescue StandardError => e
310
+ handle_collect_status_error(e, @connection.get)
311
+ end
312
+
313
+ def collect_ip_addresses=(val)
314
+ val ? @collect_ip_addresses.make_true : @collect_ip_addresses.make_false
315
+ end
316
+
317
+ def collect_topology=(val)
318
+ val ? @collect_topology.make_true : @collect_topology.make_false
319
+ end
320
+
321
+ def use_ip_address=(val)
322
+ val ? @use_ip_address.make_true : @use_ip_address.make_false
323
+ end
324
+
325
+ def interval_rate=(rate)
326
+ @interval_rate.set(rate)
327
+ notify_changes
328
+ end
329
+
330
+ def reset_collected_data
331
+ @start_ip_addresses_by_host_map.clear
332
+ @start_topology = nil
333
+ @host_names.clear
334
+ end
335
+
336
+ def notify_changes
337
+ @event.set
338
+ @event.reset
339
+ end
340
+
341
+ private
342
+
343
+ def attempt_open_connection
344
+ @connection_mutex.synchronize { @connection.set(nil) }
345
+ @panic_mode.make_true
346
+
347
+ if @connection_host_info.get.nil?
348
+ @connection_host_info.set(@initial_host)
349
+ @connected_ip_address.set(nil)
350
+ @correct_connection_host.make_false
351
+ end
352
+
353
+ host_info = @connection_host_info.get
354
+ connected_ip = @connected_ip_address.get
355
+
356
+ if @use_ip_address.true? && connected_ip
357
+ established = try_connect_on_all_instances(host_info)
358
+ unless established
359
+ clear_connection
360
+ return
361
+ end
362
+ @connection_mutex.synchronize { @connection.set(established) }
363
+ logger.debug { "[#{@role}] Opened monitoring connection (IP) to #{@connected_ip_address.get}." }
364
+ else
365
+ logger.debug { "[#{@role}] Opening monitoring connection to #{host_info.host}." }
366
+ ip = resolve_ip_address(host_info.host)
367
+ connect_props = @status_monitor_driver_props.dup
368
+ override_props = iam_enabled? ? map_merge(@status_monitor_wrapper_props, PropertyDefinition::IAM_HOST.name => host_info.host)
369
+ : @status_monitor_wrapper_props
370
+ @connection_mutex.synchronize do
371
+ @connection.set(@service_container.plugin_manager.internal_connect(
372
+ host_info, connect_props, override_props, false
373
+ ))
374
+ end
375
+ @connected_ip_address.set(ip)
376
+ logger.debug { "[#{@role}] Opened monitoring connection to #{host_info.host}." }
377
+ end
378
+
379
+ @panic_mode.make_false
380
+ notify_changes
381
+ rescue StandardError => e
382
+ logger.debug { "[#{@role}] Failed to open monitoring connection: #{e.class}: #{e.message}" }
383
+ clear_connection
384
+ end
385
+
386
+ def map_merge(base, overrides)
387
+ result = base.dup
388
+ overrides.each { |k, v| result[k] = v }
389
+ result
390
+ end
391
+
392
+ def iam_enabled?
393
+ @service_container.plugin_manager.plugin_in_use?(Plugins::IamAuthPlugin)
394
+ end
395
+
396
+ def driver_dialect
397
+ @service_container.dialect_service.driver_dialect
398
+ end
399
+
400
+ def clear_connection
401
+ @connection_mutex.synchronize { @connection.set(nil) }
402
+ @panic_mode.make_true
403
+ notify_changes
404
+ end
405
+
406
+ # Lock-free variant of clear_connection. The caller MUST already hold
407
+ # @connection_mutex
408
+ def clear_connection_locked
409
+ @connection.set(nil)
410
+ @panic_mode.make_true
411
+ notify_changes
412
+ end
413
+
414
+ def connection_closed?(conn)
415
+ driver_dialect.closed?(conn)
416
+ rescue StandardError
417
+ true
418
+ end
419
+
420
+ def syntax_error?(err)
421
+ driver_dialect.sql_state(err)&.start_with?('42') || false
422
+ end
423
+
424
+ def db_error?(err)
425
+ !driver_dialect.sql_state(err).nil?
426
+ end
427
+
428
+ # @note Callers must hold @connection_mutex (invoke within a @connection_mutex.synchronize block).
429
+ def status_available?(conn)
430
+ dialect = @service_container.dialect_service.db_dialect
431
+ return true if dialect.blue_green_status_available?(conn)
432
+
433
+ if connection_closed?(conn)
434
+ clear_connection_locked
435
+ else
436
+ @current_phase = Phase::NOT_CREATED
437
+ logger.debug { "[#{@role}] (status not available) current_phase: #{@current_phase}" }
438
+ end
439
+ false
440
+ end
441
+
442
+ def parse_status_entries(conn, dialect)
443
+ driver_dialect.execute(conn, dialect.blue_green_status_query).each_with_object([]) do |row, entries|
444
+ version = row['version']
445
+ unless KNOWN_VERSIONS.include?(version)
446
+ logger.warn do
447
+ "[#{@role}] Blue/Green deployment uses version '#{version}' which the driver does not " \
448
+ "support. Version '#{LATEST_KNOWN_VERSION}' will be used instead."
449
+ end
450
+ version = LATEST_KNOWN_VERSION
451
+ end
452
+
453
+ role = Role.parse_role(row['role'], version)
454
+ next if role != @role
455
+
456
+ entries << StatusInfo.new(version, row['endpoint'], row['port'].to_i, Phase.parse_phase(row['status']), role)
457
+ end
458
+ end
459
+
460
+ def resolve_status_info(status_entries)
461
+ info = status_entries.find { |x| Utils::RdsUtils.writer_cluster_dns?(x.endpoint) && Utils::RdsUtils.not_old_instance?(x.endpoint) }
462
+ @host_names.add(info.endpoint.downcase.gsub('.cluster-', '.cluster-ro-')) if info
463
+ info || status_entries.find { |x| Utils::RdsUtils.rds_instance?(x.endpoint) && Utils::RdsUtils.not_old_instance?(x.endpoint) }
464
+ end
465
+
466
+ def apply_status_info(status_info, status_entries)
467
+ if status_info.nil?
468
+ if status_entries.empty?
469
+ @current_phase = nil
470
+ logger.debug { "[#{@role}] No entries in status table." } if @role != Role::SOURCE
471
+ end
472
+ else
473
+ @current_phase = status_info.phase
474
+ @version = status_info.version
475
+ @port = status_info.port
476
+ end
477
+
478
+ return unless @collect_topology.true?
479
+
480
+ @host_names.merge(
481
+ status_entries.filter_map { |x| x.endpoint&.downcase if Utils::RdsUtils.not_old_instance?(x.endpoint) }
482
+ )
483
+ end
484
+
485
+ # Compares the connected IP against the endpoint IP from the status table.
486
+ # Reconnects to the correct host if a mismatch is detected.
487
+ # @note Callers must hold @connection_mutex (invoke within a @connection_mutex.synchronize block).
488
+ def verify_connection_host(status_info)
489
+ return if @correct_connection_host.true? || status_info.nil?
490
+
491
+ status_ip = resolve_ip_address(status_info.endpoint)
492
+ connected_ip = @connected_ip_address.get
493
+
494
+ if connected_ip && connected_ip != status_ip
495
+ @connection_host_info.set(
496
+ @connection_host_info.get.deep_dup(host: status_info.endpoint, port: status_info.port)
497
+ )
498
+ @correct_connection_host.make_true
499
+ clear_connection_locked
500
+ else
501
+ @correct_connection_host.make_true
502
+ @panic_mode.make_false
503
+ end
504
+ end
505
+
506
+ def handle_collect_status_error(err, conn)
507
+ if syntax_error?(err)
508
+ @current_phase = Phase::NOT_CREATED
509
+ logger.warn { "[#{@role}] current_phase: #{@current_phase}, exception while querying for blue/green status." }
510
+ elsif db_error?(err)
511
+ if err.message.include?('An error occurred while retrieving the blue/green fast switchover metadata')
512
+ @current_phase = Phase::NOT_CREATED
513
+ return
514
+ end
515
+ # If already in NOT_CREATED phase, this is the expected steady state after switchover
516
+ # (status table/function is gone). Don't enter panic mode — stay at BASELINE rate.
517
+ if @current_phase.nil? || @current_phase == Phase::NOT_CREATED
518
+ logger.debug { "[#{@role}] (status not available) current_phase: #{@current_phase}" }
519
+ return
520
+ end
521
+ logger.debug { "[#{@role}] Unhandled SQLException: #{err.message}" }
522
+ clear_connection unless connection_closed?(conn)
523
+ else
524
+ logger.debug { "[#{@role}] Unhandled exception: #{err.class}: #{err.message}" }
525
+ clear_connection
526
+ end
527
+ end
528
+
529
+ # Creates a scoped HostListProvider for this monitor using a unique cluster_id
530
+ # to avoid interfering with the application's own provider.
531
+ def init_host_list_provider
532
+ return if @host_list_provider || @correct_connection_host.false?
533
+
534
+ host_info = @connection_host_info.get
535
+ return logger.warn { 'Unable to initialize HostListProvider since connection host information is null.' } if host_info.nil?
536
+
537
+ cluster_id = "#{@bgd_id}::#{@role}::#{BG_CLUSTER}"
538
+ logger.debug { "[#{@role}] Creating a new HostListProvider, cluster_id: #{cluster_id}." }
539
+
540
+ scoped_wrapper_props = map_merge(@status_monitor_wrapper_props, PropertyDefinition::CLUSTER_ID.name => cluster_id)
541
+ scoped_wrapper_props = map_merge(scoped_wrapper_props, PropertyDefinition::IAM_HOST.name => host_info.host) if iam_enabled?
542
+
543
+ config = @service_container.connection_service.config.dup
544
+ config.initial_host_info = host_info
545
+ config.wrapper_props = scoped_wrapper_props
546
+
547
+ scoped_connection_service = Services::ConnectionService.new(@service_container, config)
548
+ scoped_container = Services::ServiceContainer.new(
549
+ scoped_connection_service,
550
+ @service_container.dialect_service,
551
+ @service_container.event_publisher,
552
+ @service_container.host_service,
553
+ @service_container.plugin_manager,
554
+ @service_container.session_state_service,
555
+ @service_container.storage_service,
556
+ @service_container.monitor_service
557
+ )
558
+
559
+ @host_list_provider = @service_container.dialect_service.db_dialect.create_host_list_provider(scoped_container)
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end