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,709 @@
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 'monitor'
18
+ require_relative 'monitor_connection'
19
+ require_relative '../host/host_role'
20
+ require_relative '../host/host_availability'
21
+ require_relative '../utils/events/monitor_reset_event'
22
+ require_relative '../utils/rds_utils'
23
+ require_relative '../property_definition'
24
+ require 'timeout'
25
+
26
+ module AwsAdvancedRubyDriverWrapper
27
+ module Monitoring
28
+ # Continuously monitors cluster topology, caches it in StorageService, and provides
29
+ # fast writer re-discovery during failover via parallel host probing (panic mode).
30
+ class ClusterTopologyMonitor < Monitor
31
+ TERMINATION_TIMEOUT_SEC = 30.0
32
+ HIGH_REFRESH_DURATION_SEC = 30.0
33
+ STABLE_TOPOLOGIES_DURATION_SEC = 15.0
34
+ TOPOLOGY_CACHE_NAME = :topology
35
+ INSTANCE_MONITOR_SLEEP_SEC = 0.1
36
+ INITIAL_BACKOFF_SEC = 0.1
37
+ MAX_BACKOFF_SEC = 10
38
+
39
+ # @param service_container [Services::ServiceContainer]
40
+ # @param cluster_id [String]
41
+ # @param instance_template [Host::HostInfo]
42
+ # @param topology_utils [Object] responds to #query_topology, #writer_instance?
43
+ # @param monitoring_driver_props [Hash] driver props with prefixed driver overrides merged in
44
+ # @param monitoring_wrapper_props [Hash] wrapper prop overrides extracted from prefixed props
45
+ def initialize(
46
+ service_container:,
47
+ cluster_id:,
48
+ instance_template:,
49
+ topology_utils:,
50
+ monitoring_driver_props:,
51
+ monitoring_wrapper_props: {}
52
+ )
53
+ super(termination_timeout_sec: TERMINATION_TIMEOUT_SEC)
54
+
55
+ @service_container = service_container
56
+ @cluster_id = cluster_id
57
+ @instance_template = instance_template
58
+ @topology_utils = topology_utils
59
+ @monitoring_driver_props = monitoring_driver_props
60
+ @monitoring_wrapper_props = monitoring_wrapper_props
61
+
62
+ # Apply default socket/connect timeouts for monitoring connections if not set by user.
63
+ service_container.dialect_service.driver_dialect.apply_monitoring_defaults(@monitoring_driver_props)
64
+
65
+ props = service_container.connection_service.wrapper_props
66
+ @refresh_rate_sec = PropertyDefinition::CLUSTER_TOPOLOGY_REFRESH_RATE_SEC.get_float(props)
67
+ @high_refresh_rate_sec = PropertyDefinition::CLUSTER_TOPOLOGY_HIGH_REFRESH_RATE_SEC.get_float(props)
68
+ @max_instance_monitors = PropertyDefinition::CLUSTER_TOPOLOGY_MAX_INSTANCE_MONITORS.get_int(props)
69
+
70
+ @monitoring_connection = MonitorConnection.new(service_container.dialect_service.driver_dialect)
71
+ @monitoring_conn_lock = Mutex.new
72
+ @writer_info = nil
73
+ @verified_writer = false
74
+ @high_refresh_end_time = 0
75
+
76
+ @topology_mutex = Mutex.new
77
+ @topology_cv = ConditionVariable.new
78
+ @update_requested = false
79
+
80
+ @instance_monitors = {} # { host_string => Thread }
81
+ @instance_monitor_connections = Concurrent::Map.new # { host_string => raw connection }
82
+ @stop_instance_monitors = false
83
+ @instance_monitors_writer_conn = MonitorConnection.new(service_container.dialect_service.driver_dialect)
84
+ @panic_mutex = Mutex.new
85
+ @instance_monitors_writer_info = nil
86
+ @instance_monitor_topologies = {}
87
+ # Tracks whether all instance monitors have completed at least one work cycle, even if an error occurs.
88
+ # This guards against concluding all instance monitor topologies are stable before all of them have initialized.
89
+ @completed_one_cycle = {}
90
+ @latest_topology = nil
91
+ @stable_start_time = 0
92
+ end
93
+
94
+ # Forces a topology refresh, ignoring any cached topology. Blocks until updated or raises on timeout.
95
+ # @param verify_writer [Boolean] if true, enters panic mode to re-verify the writer.
96
+ # @param timeout_sec [Float] max time in seconds to wait for the update.
97
+ # @return [Array<Host::HostInfo>] the updated topology.
98
+ # @raise [Timeout::Error] if the topology is not updated within timeout_sec.
99
+ def force_refresh(verify_writer, timeout_sec)
100
+ if verify_writer
101
+ if @monitoring_conn_lock.try_lock
102
+ # Monitoring thread is not mid-query; safe to close the connection.
103
+ begin
104
+ @monitoring_connection.set(nil)
105
+ ensure
106
+ @monitoring_conn_lock.unlock
107
+ end
108
+ else
109
+ # Monitoring thread is mid-query. Detach without closing — the monitoring thread will close the
110
+ # connection after its query completes (via read_timeout for MySQL or keepalives for PG).
111
+ @monitoring_connection.set(nil, close_old: false)
112
+ end
113
+ @verified_writer = false
114
+ end
115
+
116
+ wait_for_topology_update(timeout_sec, verify_writer)
117
+ end
118
+
119
+ # Event subscriber callback.
120
+ # @param event [Object]
121
+ def process_event(event)
122
+ return unless event.is_a?(Utils::Events::MonitorResetEvent)
123
+ return unless event.cluster_id == @cluster_id
124
+
125
+ reset!
126
+ end
127
+
128
+ def close
129
+ close_instance_monitors
130
+ @monitoring_conn_lock.synchronize { @monitoring_connection.close }
131
+ @instance_monitors_writer_conn.close
132
+ end
133
+
134
+ private
135
+
136
+ # Main monitoring loop.
137
+ def monitor
138
+ event_publisher.subscribe(self, Set[Utils::Events::MonitorResetEvent])
139
+ logger.debug("[#{@cluster_id}] Started cluster topology monitor")
140
+
141
+ until stopped?
142
+ update_activity
143
+
144
+ if panic_mode?
145
+ run_panic_mode_iteration
146
+ else
147
+ run_regular_mode_iteration
148
+ end
149
+ end
150
+ ensure
151
+ @stop_instance_monitors = true
152
+ close_instance_monitors
153
+ @monitoring_conn_lock.synchronize { @monitoring_connection.close }
154
+ @instance_monitors_writer_conn.close
155
+ event_publisher.unsubscribe(self, Set[Utils::Events::MonitorResetEvent])
156
+ logger.debug("[#{@cluster_id}] Stopped cluster topology monitor")
157
+ end
158
+
159
+ # --- Mode determination ---
160
+
161
+ def panic_mode?
162
+ @monitoring_connection.get.nil? || !@verified_writer
163
+ end
164
+
165
+ # --- Regular mode ---
166
+
167
+ def run_regular_mode_iteration
168
+ close_instance_monitors unless @instance_monitors.empty?
169
+
170
+ conn = @monitoring_connection.get
171
+ hosts = nil
172
+
173
+ # Hold the lock while querying so force_refresh knows not to close this connection.
174
+ @monitoring_conn_lock.synchronize do
175
+ hosts = fetch_topology_and_update_cache(conn)
176
+ end
177
+
178
+ # After releasing the lock, check if force_refresh detached this connection while
179
+ # we were querying (set it to nil with close_old: false). If so, we must close it
180
+ # since we're the only thread that held a reference during the query.
181
+ if conn && @monitoring_connection.get != conn
182
+ safe_close_connection(conn)
183
+ @verified_writer = false
184
+ @writer_info = nil
185
+ return
186
+ end
187
+
188
+ if hosts.nil?
189
+ # Unable to fetch topology. Enter panic mode.
190
+ if conn
191
+ @monitoring_connection.compare_and_set(conn, nil)
192
+ safe_close_connection(conn)
193
+ end
194
+ @verified_writer = false
195
+ @writer_info = nil
196
+ return
197
+ end
198
+
199
+ @high_refresh_end_time = 0 if @high_refresh_end_time.positive? && monotonic_time > @high_refresh_end_time
200
+
201
+ delay(use_high_rate: @high_refresh_end_time&.positive?)
202
+ end
203
+
204
+ # --- Panic mode ---
205
+
206
+ def run_panic_mode_iteration
207
+ if @instance_monitors.empty?
208
+ start_instance_monitors
209
+ else
210
+ check_new_hosts_for_instance_monitors unless writer_picked_up_from_instance_monitors?
211
+ check_stable_instance_monitor_topologies
212
+ end
213
+
214
+ delay(use_high_rate: true)
215
+ end
216
+
217
+ def start_instance_monitors
218
+ @instance_monitors_writer_info = nil
219
+ @latest_topology = nil
220
+ cleanup_host_writer_connection
221
+
222
+ hosts = stored_hosts || open_any_connection_and_update_topology
223
+ # Close any previously running instance monitors.
224
+ close_instance_monitors
225
+ @stop_instance_monitors = false
226
+
227
+ return if hosts.nil? || @verified_writer
228
+
229
+ hosts.first(@max_instance_monitors).each do |host_info|
230
+ spawn_instance_monitor(host_info)
231
+ end
232
+ end
233
+
234
+ # Returns true if writer was picked up from instance monitors (caller should skip further checks).
235
+ def writer_picked_up_from_instance_monitors?
236
+ writer_conn = @instance_monitors_writer_conn.get
237
+ writer_host = @instance_monitors_writer_info
238
+ return false unless writer_conn && writer_host
239
+
240
+ logger.info("[#{@cluster_id}] Writer found: #{writer_host.host}")
241
+ @monitoring_conn_lock.synchronize { @monitoring_connection.set(writer_conn, close_old: true) }
242
+ # Avoid double-close: clear host reference without closing since we transferred ownership.
243
+ @instance_monitors_writer_conn.set(nil, close_old: false)
244
+ @writer_info = writer_host
245
+ @verified_writer = true
246
+ @high_refresh_end_time = monotonic_time + HIGH_REFRESH_DURATION_SEC
247
+
248
+ @stop_instance_monitors = true
249
+ close_instance_monitors
250
+ true
251
+ end
252
+
253
+ def check_new_hosts_for_instance_monitors
254
+ hosts = @latest_topology
255
+ return if hosts.nil? || @stop_instance_monitors
256
+
257
+ hosts.first(@max_instance_monitors).each do |host_info|
258
+ spawn_instance_monitor(host_info) unless @instance_monitors.key?(host_info.host) ||
259
+ @instance_monitors.size >= @max_instance_monitors
260
+ end
261
+ end
262
+
263
+ def spawn_instance_monitor(host_info)
264
+ thread = Thread.new { instance_monitor(host_info) }
265
+ thread.name = "instance-monitor-#{host_info.host}"
266
+ @instance_monitors[host_info.host] = thread
267
+ end
268
+
269
+ # --- Stable reader topologies consensus ---
270
+
271
+ def check_stable_instance_monitor_topologies
272
+ hosts = stored_hosts
273
+ if hosts.nil? || hosts.empty?
274
+ reset_stable_state
275
+ return
276
+ end
277
+
278
+ monitored_ids = hosts.first(@max_instance_monitors).map(&:id)
279
+
280
+ completed, topologies = @panic_mutex.synchronize { [@completed_one_cycle.dup, @instance_monitor_topologies.dup] }
281
+
282
+ unless monitored_ids.all? { |id| completed[id] }
283
+ reset_stable_state
284
+ return
285
+ end
286
+
287
+ if topologies.empty?
288
+ reset_stable_state
289
+ return
290
+ end
291
+
292
+ # Check if all instance monitor topologies match (by host, port, role, availability).
293
+ reference_topology = topologies.values.first
294
+ all_match = topologies.values.all? do |topo|
295
+ topo.size == reference_topology.size && topo.zip(reference_topology).all? do |a, b|
296
+ a.host == b.host && a.port == b.port && a.role == b.role && a.availability == b.availability
297
+ end
298
+ end
299
+
300
+ unless all_match
301
+ reset_stable_state
302
+ return
303
+ end
304
+
305
+ @stable_start_time = monotonic_time if @stable_start_time.zero?
306
+
307
+ return unless monotonic_time > @stable_start_time + STABLE_TOPOLOGIES_DURATION_SEC
308
+
309
+ @stable_start_time = 0
310
+ update_hosts_availability(reference_topology)
311
+ update_topology_cache(reference_topology)
312
+ logger.debug("[#{@cluster_id}] Stable instance monitor topologies accepted")
313
+ end
314
+
315
+ def reset_stable_state
316
+ @stable_start_time = 0
317
+ end
318
+
319
+ # --- Instance Monitor ---
320
+
321
+ def instance_monitor(host_info)
322
+ conn = nil
323
+ connection_attempts = 0
324
+ writer_changed = false
325
+
326
+ until @stop_instance_monitors
327
+ if conn.nil?
328
+ conn = attempt_host_connection(host_info, connection_attempts)
329
+ if conn.nil?
330
+ connection_attempts += 1
331
+ @panic_mutex.synchronize do
332
+ @completed_one_cycle[host_info.id] = true
333
+ @instance_monitor_topologies.delete(host_info.id)
334
+ end
335
+ next
336
+ end
337
+ connection_attempts = 0
338
+ @instance_monitor_connections[host_info.host] = conn
339
+ end
340
+
341
+ # If the connection was closed externally abandon it and reconnect on the next iteration.
342
+ if @service_container.dialect_service.driver_dialect.closed?(conn)
343
+ conn = nil
344
+ @instance_monitor_connections.delete(host_info.host)
345
+ next
346
+ end
347
+
348
+ role = check_host_role(conn)
349
+ if role.nil?
350
+ safe_close_connection(conn)
351
+ conn = nil
352
+ @instance_monitor_connections.delete(host_info.host)
353
+ next
354
+ end
355
+
356
+ if role == Host::HostRole::WRITER
357
+ handle_writer_found(host_info, conn)
358
+ return
359
+ end
360
+
361
+ # Reader path — writer_changed is sticky: once true, all subsequent fetches update the cache immediately.
362
+ writer_changed = reader_fetch_topology(conn, host_info, writer_changed)
363
+ @panic_mutex.synchronize { @completed_one_cycle[host_info.id] = true }
364
+ sleep(INSTANCE_MONITOR_SLEEP_SEC)
365
+ end
366
+ rescue StandardError => e
367
+ logger.debug("[#{@cluster_id}] Instance Monitor #{host_info.host}: #{e.message}")
368
+ ensure
369
+ @instance_monitor_connections.delete(host_info.host)
370
+ @panic_mutex.synchronize do
371
+ @completed_one_cycle[host_info.id] = true
372
+ @instance_monitor_topologies.delete(host_info.id)
373
+ end
374
+ safe_close_connection(conn) if conn && conn != @instance_monitors_writer_conn.get
375
+ end
376
+
377
+ def attempt_host_connection(host_info, attempts)
378
+ internal_connect(host_info)
379
+ rescue StandardError => e
380
+ raise if @service_container.dialect_service.login_error?(e)
381
+
382
+ logger.debug("[#{@cluster_id}] Connection to #{host_info.host} failed: #{e.message}")
383
+ sleep(calculate_backoff(attempts))
384
+ nil
385
+ end
386
+
387
+ def check_host_role(conn)
388
+ @service_container.dialect_service.db_dialect.host_role(conn)
389
+ rescue StandardError => e
390
+ logger.debug("[#{@cluster_id}] host_role check failed: #{e.message}")
391
+ nil
392
+ end
393
+
394
+ def handle_writer_found(host_info, conn)
395
+ # If compare_and_set fails, another instance monitor already found the writer. Connection will be closed in
396
+ # caller's ensure block.
397
+ return unless @instance_monitors_writer_conn.compare_and_set(nil, conn)
398
+
399
+ fetch_topology_and_update_cache(conn)
400
+ host_info.availability = Host::HostAvailability::AVAILABLE
401
+ @instance_monitors_writer_info = host_info
402
+ @stop_instance_monitors = true
403
+ logger.info("[#{@cluster_id}] Writer verified: #{host_info.host}")
404
+ end
405
+
406
+ def reader_fetch_topology(conn, host_info, writer_changed)
407
+ hosts = query_topology(conn)
408
+ return writer_changed if hosts.nil?
409
+
410
+ @panic_mutex.synchronize do
411
+ @latest_topology = hosts
412
+ @instance_monitor_topologies[host_info.id] = hosts
413
+ end
414
+
415
+ if writer_changed
416
+ update_hosts_availability(hosts)
417
+ update_topology_cache(hosts)
418
+ return true
419
+ end
420
+
421
+ latest_writer = hosts.find { |h| h.role == Host::HostRole::WRITER }
422
+ if latest_writer && @writer_info &&
423
+ latest_writer.host != @writer_info&.host
424
+ logger.info("[#{@cluster_id}] Writer changed: #{@writer_info&.host} -> #{latest_writer.host}")
425
+ update_hosts_availability(hosts)
426
+ update_topology_cache(hosts)
427
+ return true
428
+ end
429
+
430
+ writer_changed
431
+ end
432
+
433
+ # --- Topology operations ---
434
+
435
+ def open_any_connection_and_update_topology
436
+ @monitoring_conn_lock.synchronize do
437
+ existing_conn = @monitoring_connection.get
438
+ return fetch_topology_and_update_cache(existing_conn) if existing_conn
439
+ end
440
+
441
+ conn = internal_connect(initial_host_info)
442
+ return nil if conn.nil?
443
+
444
+ @monitoring_conn_lock.synchronize do
445
+ unless @monitoring_connection.compare_and_set(nil, conn)
446
+ safe_close_connection(conn)
447
+ return fetch_topology_and_update_cache(@monitoring_connection.get)
448
+ end
449
+
450
+ hosts = fetch_topology_and_update_cache(conn)
451
+ if hosts.nil?
452
+ @monitoring_connection.set(nil)
453
+ @verified_writer = false
454
+ @writer_info = nil
455
+ return nil
456
+ end
457
+
458
+ # When the connected endpoint is the writer, record the writer as the actual instance from the
459
+ # fetched topology rather than initial_host_info, which is typically the cluster endpoint. The
460
+ # writer-verification check compares against instance-level topology entries, so recording the
461
+ # cluster endpoint (which never appears in the topology) would block verification until timeout.
462
+ if check_host_role(conn) == Host::HostRole::WRITER
463
+ writer = hosts.find { |h| h.role == Host::HostRole::WRITER }
464
+ if writer
465
+ @verified_writer = true
466
+ @writer_info = writer
467
+ end
468
+ end
469
+
470
+ hosts
471
+ end
472
+ rescue StandardError => e
473
+ logger.debug("[#{@cluster_id}] Failed to open monitoring connection: #{e.message}")
474
+ nil
475
+ end
476
+
477
+ def fetch_topology_and_update_cache(conn)
478
+ return nil if conn.nil?
479
+
480
+ driver_dialect = @service_container.dialect_service.driver_dialect
481
+ if driver_dialect.closed?(conn)
482
+ logger.debug("[#{@cluster_id}] Monitoring connection is closed; skipping topology fetch")
483
+ return nil
484
+ end
485
+
486
+ hosts = query_topology(conn)
487
+ update_topology_cache(hosts) if hosts && !hosts.empty?
488
+ hosts
489
+ rescue StandardError => e
490
+ logger.debug("[#{@cluster_id}] Topology fetch failed: #{e.message}")
491
+ nil
492
+ end
493
+
494
+ def query_topology(conn)
495
+ @topology_utils.query_topology(conn, @initial_host_info, @instance_template)
496
+ end
497
+
498
+ def stored_hosts
499
+ storage_service.get(TOPOLOGY_CACHE_NAME, @cluster_id, register_access: false)
500
+ end
501
+
502
+ def update_topology_cache(hosts)
503
+ @topology_mutex.synchronize do
504
+ storage_service.set(TOPOLOGY_CACHE_NAME, @cluster_id, hosts)
505
+ @update_requested = false
506
+ @topology_cv.broadcast
507
+ end
508
+ end
509
+
510
+ def clear_topology_cache
511
+ @topology_mutex.synchronize do
512
+ storage_service.remove(TOPOLOGY_CACHE_NAME, @cluster_id)
513
+ @update_requested = false
514
+ @topology_cv.broadcast
515
+ end
516
+ end
517
+
518
+ def update_hosts_availability(hosts)
519
+ return if hosts.nil? || hosts.empty?
520
+
521
+ hosts.each do |host|
522
+ available = @instance_monitor_topologies.key?(host.id)
523
+ host.availability = available ? Host::HostAvailability::AVAILABLE : Host::HostAvailability::UNAVAILABLE
524
+ end
525
+ end
526
+
527
+ # --- force_refresh support ---
528
+
529
+ def wait_for_topology_update(timeout_sec, verify_writer = false)
530
+ current_hosts = stored_hosts
531
+
532
+ @topology_mutex.synchronize do
533
+ @update_requested = true
534
+ @topology_cv.broadcast
535
+ end
536
+
537
+ return current_hosts if timeout_sec.zero?
538
+
539
+ deadline = monotonic_time + timeout_sec
540
+ @topology_mutex.synchronize do
541
+ # We are checking reference equality instead of value equality. We will break out of the loop if there is a
542
+ # new entry in the topology cache, even if current_hosts contains the same hosts as stored_hosts.
543
+ #
544
+ # When verify_writer is set, a fresh cache entry is not enough: right after failover the topology query can
545
+ # still report the demoted writer as the writer (e.g. Aurora MySQL's replica_host_status is eventually
546
+ # consistent), so we keep waiting until the cached writer matches the writer this monitor has independently
547
+ # verified by probing instances directly, or the timeout is hit.
548
+ while wait_for_topology?(current_hosts, verify_writer) && monotonic_time < deadline && !stopped?
549
+ remaining = deadline - monotonic_time
550
+ break if remaining <= 0
551
+
552
+ @topology_cv.wait(@topology_mutex, remaining)
553
+ end
554
+ end
555
+
556
+ latest = stored_hosts
557
+ if latest.equal?(current_hosts)
558
+ raise Timeout::Error, "Topology not updated within #{timeout_sec}s for cluster #{@cluster_id}"
559
+ elsif verify_writer && !cached_writer_verified?(latest)
560
+ raise Timeout::Error, "Topology writer did not match the verified writer within #{timeout_sec}s for cluster #{@cluster_id}"
561
+ end
562
+
563
+ latest
564
+ end
565
+
566
+ # Returns true while force_refresh should keep waiting for a topology update.
567
+ def wait_for_topology?(current_hosts, verify_writer)
568
+ return true if stored_hosts.equal?(current_hosts)
569
+
570
+ # A new cache entry arrived. When verifying the writer, keep waiting until the topology writer matches the
571
+ # writer the monitor has verified directly.
572
+ verify_writer && !cached_writer_verified?(stored_hosts)
573
+ end
574
+
575
+ # Returns true when the writer in the given topology matches the writer this monitor has verified by probing
576
+ # instances directly. Returns false when either is missing so the caller keeps waiting.
577
+ def cached_writer_verified?(hosts)
578
+ return false unless @verified_writer
579
+
580
+ verified_writer = @writer_info
581
+ return false if verified_writer.nil? || hosts.nil?
582
+
583
+ cached_writer = hosts.find { |h| h.role == Host::HostRole::WRITER }
584
+ return false if cached_writer.nil?
585
+
586
+ # During a Blue/Green switchover the freshly cached topology is read from a green node, so its
587
+ # writer carries a green-prefixed identifier (e.g. "instance-green-abc123") while the writer this
588
+ # monitor verified by probing is the blue identifier. Strip the green marker from both sides so
589
+ # the same logical instance matches across the rename.
590
+ return true if green_normalized(cached_writer.host) == green_normalized(verified_writer.host)
591
+
592
+ cached_writer.id && verified_writer.id &&
593
+ green_normalized(cached_writer.id) == green_normalized(verified_writer.id)
594
+ end
595
+
596
+ # Strips a Blue/Green "-green-xxxxxx" marker from a host or instance id so blue and green
597
+ # identifiers for the same instance compare equal. Non-green values are returned unchanged.
598
+ def green_normalized(value)
599
+ return value if value.nil?
600
+
601
+ Utils::RdsUtils.remove_green_instance_prefix(value)
602
+ end
603
+
604
+ # --- Reset ---
605
+
606
+ def reset!
607
+ logger.debug("[#{@cluster_id}] Monitor reset")
608
+ @stop_instance_monitors = true
609
+ close_instance_monitor_connections
610
+ close_instance_monitors
611
+ @stop_instance_monitors = false
612
+ @monitoring_conn_lock.synchronize { @monitoring_connection.set(nil) }
613
+ @verified_writer = false
614
+ @writer_info = nil
615
+ @high_refresh_end_time = 0
616
+ clear_topology_cache
617
+
618
+ @topology_mutex.synchronize do
619
+ @update_requested = true
620
+ @topology_cv.broadcast
621
+ end
622
+ end
623
+
624
+ # --- Cleanup helpers ---
625
+
626
+ # Closes all instance monitor connections so that any in-flight queries exits safely.
627
+ # This MUST be called before join/kill on the threads.
628
+ def close_instance_monitor_connections
629
+ @instance_monitor_connections.each_pair do |host, conn|
630
+ safe_close_connection(conn)
631
+ @instance_monitor_connections.delete(host)
632
+ end
633
+ end
634
+
635
+ def close_instance_monitors
636
+ @stop_instance_monitors = true
637
+ @instance_monitors.each_value do |t|
638
+ t.join(5)
639
+ t.kill if t.alive?
640
+ end
641
+ @instance_monitors.clear
642
+ cleanup_host_writer_connection
643
+ @stable_start_time = 0
644
+ @instance_monitor_topologies.clear
645
+ @completed_one_cycle.clear
646
+ end
647
+
648
+ def cleanup_host_writer_connection
649
+ # Avoid double-close if the monitoring connection already owns this reference.
650
+ @monitoring_conn_lock.synchronize do
651
+ if @monitoring_connection.get.equal?(@instance_monitors_writer_conn.get)
652
+ @instance_monitors_writer_conn.set(nil, close_old: false)
653
+ else
654
+ @instance_monitors_writer_conn.set(nil)
655
+ end
656
+ end
657
+ end
658
+
659
+ # --- Delay ---
660
+
661
+ def delay(use_high_rate:)
662
+ use_high_rate = true if @high_refresh_end_time.positive? && monotonic_time < @high_refresh_end_time
663
+
664
+ @topology_mutex.synchronize do
665
+ if @update_requested
666
+ use_high_rate = true
667
+ @update_requested = false
668
+ end
669
+ duration = use_high_rate ? @high_refresh_rate_sec : @refresh_rate_sec
670
+ @topology_cv.wait(@topology_mutex, duration) unless stopped?
671
+ end
672
+ end
673
+
674
+ # --- Utilities ---
675
+
676
+ def monotonic_time
677
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
678
+ end
679
+
680
+ def safe_close_connection(conn)
681
+ @service_container.dialect_service.driver_dialect.close_connection(conn) if conn
682
+ rescue StandardError
683
+ nil
684
+ end
685
+
686
+ def calculate_backoff(attempt)
687
+ backoff = INITIAL_BACKOFF_SEC * (2**[attempt, 6].min)
688
+ backoff = [backoff, MAX_BACKOFF_SEC].min
689
+ backoff * (0.5 + (rand * 0.5))
690
+ end
691
+
692
+ def event_publisher
693
+ @service_container.event_publisher
694
+ end
695
+
696
+ def storage_service
697
+ @service_container.storage_service
698
+ end
699
+
700
+ def initial_host_info
701
+ @service_container.connection_service.initial_host_info
702
+ end
703
+
704
+ def internal_connect(host_info)
705
+ @service_container.plugin_manager.internal_connect(host_info, @monitoring_driver_props.dup, @monitoring_wrapper_props, false)
706
+ end
707
+ end
708
+ end
709
+ end