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,267 @@
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 '../db_dialects/aurora_mysql_dialect'
18
+ require_relative '../db_dialects/aurora_pg_dialect'
19
+ require_relative '../db_dialects/dialect_codes'
20
+ require_relative '../db_dialects/global_mysql_dialect'
21
+ require_relative '../db_dialects/global_pg_dialect'
22
+ require_relative '../db_dialects/multi_az_cluster_mysql_dialect'
23
+ require_relative '../db_dialects/multi_az_cluster_pg_dialect'
24
+ require_relative '../db_dialects/mysql_dialect'
25
+ require_relative '../db_dialects/pg_dialect'
26
+ require_relative '../db_dialects/rds_mysql_dialect'
27
+ require_relative '../db_dialects/rds_pg_dialect'
28
+ require_relative '../db_dialects/unknown_dialect'
29
+ require_relative '../driver_dialects/driver_dialect_manager'
30
+ require_relative '../errors'
31
+ require_relative '../property_definition'
32
+ require_relative '../utils/rds_url_type'
33
+ require_relative '../utils/rds_utils'
34
+ require_relative '../utils/storage/expiration_cache'
35
+
36
+ module AwsAdvancedRubyDriverWrapper
37
+ module Services
38
+ class DialectService
39
+ attr_reader :driver_dialect, :db_dialect
40
+
41
+ ENDPOINT_CACHE_EXPIRATION = 86_400 # 24 hours in seconds
42
+
43
+ @known_endpoint_dialects = Utils::Storage::ExpirationCache.new(ttl: ENDPOINT_CACHE_EXPIRATION)
44
+
45
+ class << self
46
+ attr_reader :known_endpoint_dialects
47
+ end
48
+
49
+ KNOWN_DIALECT_CLASSES = {
50
+ DialectCodes::MYSQL => DbDialects::MysqlDialect,
51
+ DialectCodes::RDS_MYSQL => DbDialects::RdsMysqlDialect,
52
+ DialectCodes::AURORA_MYSQL => DbDialects::AuroraMysqlDialect,
53
+ DialectCodes::GLOBAL_AURORA_MYSQL => DbDialects::GlobalMysqlDialect,
54
+ DialectCodes::MULTI_AZ_MYSQL_CLUSTER => DbDialects::MultiAzClusterMysqlDialect,
55
+ DialectCodes::PG => DbDialects::PgDialect,
56
+ DialectCodes::RDS_PG => DbDialects::RdsPgDialect,
57
+ DialectCodes::AURORA_PG => DbDialects::AuroraPgDialect,
58
+ DialectCodes::GLOBAL_AURORA_PG => DbDialects::GlobalPgDialect,
59
+ DialectCodes::MULTI_AZ_PG_CLUSTER => DbDialects::MultiAzClusterPgDialect,
60
+ DialectCodes::UNKNOWN => DbDialects::UnknownDialect
61
+ }.freeze
62
+
63
+ # @param connection_service [ConnectionService]
64
+ # @param driver_name [Symbol] :mysql2 or :postgresql
65
+ def initialize(connection_service, driver_name)
66
+ @connection_service = connection_service
67
+ @dialect_cache = {}
68
+ @can_update = false
69
+ @driver_dialect = DriverDialects::DriverDialectManager.get_dialect(driver_name)
70
+ @error_handler = AwsAdvancedRubyDriverWrapper.config.custom_error_handler ||
71
+ DriverDialects::DriverDialectManager.get_error_handler(driver_name)
72
+ @db_dialect = init_dialect
73
+ end
74
+
75
+ # @return [Boolean] whether the dialect is known for certain and will not be swapped. This
76
+ # is the true when the dialect can no longer be updated, either because it was set
77
+ # explicitly / resolved from an unambiguous URL (such as a global writer cluster endpoint)
78
+ # or because it has already been confirmed via live connection queries. When true, topology
79
+ # fetches may proceed.
80
+ def dialect_final?
81
+ !@can_update
82
+ end
83
+
84
+ # Lazily instantiates and caches a dialect by code.
85
+ #
86
+ # @param code [String] the dialect code
87
+ # @return [Object, nil] the dialect instance or nil if unknown
88
+ def dialect_for_code(code)
89
+ @dialect_cache[code] ||= begin
90
+ known_class = KNOWN_DIALECT_CLASSES[code]
91
+ known_class&.new(@driver_dialect)
92
+ end
93
+ end
94
+
95
+ # Refines the dialect after a connection is established by querying the server
96
+ # (e.g. checking for Aurora-specific functions/tables).
97
+ #
98
+ # @param connection [Object] the live database connection
99
+ # @return [Object] the updated database dialect
100
+ def update_dialect(connection)
101
+ original_dialect_code = @dialect_code
102
+
103
+ if @can_update
104
+ host = @connection_service.initial_host_info&.host
105
+ host_url = @connection_service.initial_host_info&.url
106
+
107
+ candidates = @db_dialect.dialect_update_candidates
108
+ candidates&.each do |candidate_code|
109
+ candidate = dialect_for_code(candidate_code)
110
+ raise Errors::AwsError, "Unknown dialect code: #{candidate_code}" unless candidate
111
+
112
+ next unless candidate.dialect?(connection)
113
+
114
+ @can_update = false
115
+ @dialect_code = candidate_code
116
+ @db_dialect = candidate
117
+
118
+ self.class.known_endpoint_dialects.put(host, candidate_code) if host
119
+ self.class.known_endpoint_dialects.put(host_url, candidate_code) if host_url
120
+
121
+ break
122
+ end
123
+
124
+ if @can_update
125
+ # No candidate matched
126
+ raise Errors::AwsError, 'Unable to determine dialect' if @dialect_code == DialectCodes::UNKNOWN
127
+
128
+ @can_update = false
129
+ self.class.known_endpoint_dialects.put(host, @dialect_code) if host
130
+ self.class.known_endpoint_dialects.put(host_url, @dialect_code) if host_url
131
+ end
132
+ end
133
+
134
+ swap_host_list_provider if @dialect_code != original_dialect_code
135
+ @service_container.host_service.refresh_host_list
136
+ @db_dialect
137
+ end
138
+
139
+ # Creates the initial host list provider from the URL-guessed dialect.
140
+ # Called after the service container is fully assembled.
141
+ #
142
+ # @param service_container [ServiceContainer]
143
+ def setup_initial_provider(service_container)
144
+ @service_container = service_container
145
+ provider = @db_dialect.create_host_list_provider(service_container)
146
+ service_container.host_service.host_list_provider = provider if provider
147
+ end
148
+
149
+ # @param error [Exception]
150
+ # @return [Boolean]
151
+ def network_error?(error)
152
+ @error_handler.network_error?(error)
153
+ end
154
+
155
+ # @param error [Exception]
156
+ # @return [Boolean]
157
+ def login_error?(error)
158
+ @error_handler.login_error?(error)
159
+ end
160
+
161
+ # @param error [Exception]
162
+ # @return [Boolean]
163
+ def read_only_error?(error)
164
+ @error_handler.read_only_error?(error)
165
+ end
166
+
167
+ private
168
+
169
+ # Resolves the initial database dialect from the initial connection info.
170
+ # Uses RdsUtils to classify the host (Aurora cluster, RDS instance, etc.)
171
+ # and selects the appropriate dialect.
172
+ #
173
+ # @return [Object] the resolved database dialect
174
+ def init_dialect
175
+ @can_update = false
176
+ @db_dialect = nil
177
+
178
+ # Check for a custom dialect object injected via config
179
+ custom = AwsAdvancedRubyDriverWrapper.config.custom_dialect
180
+ if custom
181
+ @dialect_code = :custom
182
+ @db_dialect = custom
183
+ @can_update = false
184
+ return @db_dialect
185
+ end
186
+
187
+ user_dialect_setting = PropertyDefinition::DIALECT.get(@connection_service.wrapper_props)&.to_s
188
+ user_dialect_setting = user_dialect_setting&.strip&.downcase
189
+ host = @connection_service.initial_host_info&.host
190
+
191
+ dialect_code = if user_dialect_setting.nil? || user_dialect_setting.empty?
192
+ self.class.known_endpoint_dialects.get(host) unless host.nil?
193
+ else
194
+ user_dialect_setting
195
+ end
196
+
197
+ if dialect_code
198
+ dialect = dialect_for_code(dialect_code)
199
+ raise Errors::AwsError, "Unknown dialect code: #{dialect_code}" unless dialect
200
+
201
+ @dialect_code = dialect_code
202
+ @db_dialect = dialect
203
+ return @db_dialect
204
+ end
205
+
206
+ rds_type = Utils::RdsUtils.identify_rds_type(host)
207
+
208
+ @dialect_code = if @driver_dialect == DriverDialects::DriverDialectManager::MYSQL_DIALECT
209
+ resolve_mysql_dialect(rds_type)
210
+ elsif @driver_dialect == DriverDialects::DriverDialectManager::PG_DIALECT
211
+ resolve_pg_dialect(rds_type)
212
+ else
213
+ DialectCodes::UNKNOWN
214
+ end
215
+
216
+ @db_dialect = dialect_for_code(@dialect_code)
217
+ @db_dialect
218
+ end
219
+
220
+ def resolve_mysql_dialect(rds_type)
221
+ if rds_type == Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
222
+ @can_update = false
223
+ DialectCodes::GLOBAL_AURORA_MYSQL
224
+ elsif rds_type.rds_cluster?
225
+ @can_update = true
226
+ DialectCodes::AURORA_MYSQL
227
+ elsif rds_type.rds?
228
+ @can_update = true
229
+ DialectCodes::RDS_MYSQL
230
+ else
231
+ @can_update = true
232
+ DialectCodes::MYSQL
233
+ end
234
+ end
235
+
236
+ def resolve_pg_dialect(rds_type)
237
+ if rds_type == Utils::RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
238
+ @can_update = false
239
+ DialectCodes::GLOBAL_AURORA_PG
240
+ elsif rds_type == Utils::RdsUrlType::RDS_AURORA_LIMITLESS_DB_SHARD_GROUP
241
+ @can_update = false
242
+ DialectCodes::AURORA_PG
243
+ elsif rds_type.rds_cluster?
244
+ @can_update = true
245
+ DialectCodes::AURORA_PG
246
+ elsif rds_type.rds?
247
+ @can_update = true
248
+ DialectCodes::RDS_PG
249
+ else
250
+ @can_update = true
251
+ DialectCodes::PG
252
+ end
253
+ end
254
+
255
+ def swap_host_list_provider
256
+ return unless @service_container
257
+
258
+ host_service = @service_container.host_service
259
+ old_provider = host_service.host_list_provider
260
+ old_provider&.stop_monitor
261
+
262
+ new_provider = @db_dialect.create_host_list_provider(@service_container)
263
+ host_service.host_list_provider = new_provider
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,199 @@
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/map'
18
+ require_relative '../host/random_host_selector'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Services
22
+ class HostService
23
+ DEFAULT_HOST_SELECTORS = {
24
+ Host::RandomHostSelector::STRATEGY_NAME => Host::RandomHostSelector.new
25
+ }.freeze
26
+
27
+ attr_accessor :host_list_provider
28
+
29
+ @host_id_cache = Concurrent::Map.new
30
+ @strategies = Concurrent::Map.new
31
+ DEFAULT_HOST_SELECTORS.each { |name, selector| @strategies[name] = selector }
32
+
33
+ def initialize(service_container)
34
+ @service_container = service_container
35
+ @all_hosts = []
36
+ @availability_cache = Utils::Storage::ExpirationCache.new
37
+ @host_list_provider = nil
38
+ end
39
+
40
+ class << self
41
+ attr_reader :host_id_cache
42
+
43
+ def clear_id_cache
44
+ @host_id_cache.clear
45
+ end
46
+
47
+ # Register a non-default host selector. The selector is shared by every HostService in the process,
48
+ # so it must be safe to call from multiple threads.
49
+ #
50
+ # @param name [String] strategy name
51
+ # @param selector [#select_host] any object responding to select_host(hosts, role, props)
52
+ def register_host_selector(name, selector)
53
+ raise Errors::AwsError, "Cannot override default host selection strategy: '#{name}'" if DEFAULT_HOST_SELECTORS.key?(name)
54
+
55
+ @strategies[name] = selector
56
+ end
57
+
58
+ # @param name [String] strategy name
59
+ # @return [#select_host, nil] the registered selector, or nil if the name is unknown
60
+ def host_selector(name)
61
+ @strategies[name]
62
+ end
63
+
64
+ # Removes every non-default host selector. For testing only.
65
+ # @api private
66
+ def reset_host_selectors
67
+ # Snapshot the names first rather than iterating the map while deleting from it.
68
+ custom_names = @strategies.keys.reject { |name| DEFAULT_HOST_SELECTORS.key?(name) }
69
+ custom_names.each { |name| @strategies.delete(name) }
70
+ end
71
+ end
72
+
73
+ # @param hosts [Array<Host::HostInfo>]
74
+ # @param role [Symbol, nil]
75
+ # @param strategy [String]
76
+ # @param props [Hash, nil]
77
+ # @return [Host::HostInfo]
78
+ def select_host(hosts, role, strategy, props = nil)
79
+ normalized = strategy.to_s.strip.downcase
80
+ selector = self.class.host_selector(normalized)
81
+ raise Errors::AwsError, "Unsupported host selection strategy: '#{strategy}'" if selector.nil?
82
+
83
+ selector.select_host(hosts, role, props)
84
+ end
85
+
86
+ # @return [Array<Host::HostInfo>] all hosts in the topology, including blocked/unavailable
87
+ attr_reader :all_hosts
88
+
89
+ # @return [Array<Host::HostInfo>] hosts filtered by allowed/blocked rules from the custom endpoint plugin
90
+ def hosts
91
+ rules = @service_container.storage_service.get_if_registered(
92
+ :custom_endpoint_allowed_blocked,
93
+ @service_container.connection_service.initial_host_info&.url,
94
+ register_access: false
95
+ )
96
+ return @all_hosts if rules.nil?
97
+
98
+ allowed = rules[:allowed]
99
+ blocked = rules[:blocked]
100
+ required_role = rules[:required_role]
101
+
102
+ hosts = @all_hosts
103
+ hosts = hosts.select { |h| allowed.include?(h.id) } if allowed
104
+ hosts = hosts.reject { |h| h.id && blocked.include?(h.id) } if blocked
105
+ hosts = hosts.select { |h| h.role == required_role } if required_role
106
+ hosts
107
+ end
108
+
109
+ # Updates the availability of a host in the internal host list.
110
+ #
111
+ # @param host_info [HostInfo] the host whose availability has been determined
112
+ # @param availability [Symbol] the new availability status, e.g. :available or :unavailable
113
+ def set_availability(host_info, availability)
114
+ host = @all_hosts.find { |h| h.id == host_info.id || h.host.casecmp?(host_info.host) }
115
+ return if host.nil?
116
+
117
+ host.availability = availability
118
+ @availability_cache.put(host_info.url, availability)
119
+ end
120
+
121
+ # Refresh the host list from the host list provider.
122
+ def refresh_host_list
123
+ updated_hosts = @host_list_provider&.refresh
124
+ return if updated_hosts.nil? || updated_hosts == @all_hosts
125
+
126
+ apply_cached_availability(updated_hosts)
127
+ @all_hosts = updated_hosts
128
+ end
129
+
130
+ # Force a refresh of the host list, bypassing any caching.
131
+ #
132
+ # @param verify_writer [Boolean]
133
+ # @param timeout_sec [Float]
134
+ # @return [Boolean] whether the refresh was successful
135
+ def force_refresh_host_list?(verify_writer: false, timeout_sec: 5.0)
136
+ updated_hosts = @host_list_provider&.force_refresh(verify_writer, timeout_sec)
137
+ return false if updated_hosts.nil?
138
+
139
+ if updated_hosts != @all_hosts
140
+ apply_cached_availability(updated_hosts)
141
+ @all_hosts = updated_hosts
142
+ end
143
+
144
+ true
145
+ end
146
+
147
+ # Identify which host in the topology a given connection belongs to.
148
+ #
149
+ # @param connection [Object]
150
+ # @param connection_host_info [Host::HostInfo, nil] the host info used to establish the connection
151
+ # @return [Host::HostInfo, nil]
152
+ def identify_host(connection, connection_host_info = nil)
153
+ return find_host(*query_id_and_name(connection)) if connection_host_info.nil?
154
+
155
+ url_type = Utils::RdsUtils.identify_rds_type(connection_host_info&.host)
156
+ case url_type
157
+ when Utils::RdsUrlType::RDS_INSTANCE
158
+ connection_host_info
159
+ when Utils::RdsUrlType::IP_ADDRESS, Utils::RdsUrlType::OTHER
160
+ get_cached_host_info(connection, connection_host_info)
161
+ else
162
+ find_host(*query_id_and_name(connection))
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def apply_cached_availability(hosts)
169
+ hosts.each do |host|
170
+ availability = @availability_cache.get(host.url)
171
+ next if availability.nil?
172
+
173
+ host.availability = availability
174
+ end
175
+ end
176
+
177
+ def get_cached_host_info(connection, connection_host_info)
178
+ host = connection_host_info.host
179
+ instance_id, instance_name = self.class.host_id_cache.compute_if_absent(host) do
180
+ query_id_and_name(connection)
181
+ end
182
+ find_host(instance_id, instance_name)
183
+ end
184
+
185
+ def query_id_and_name(connection)
186
+ @service_container.dialect_service.db_dialect.instance_identity(connection)
187
+ rescue StandardError
188
+ [nil, nil]
189
+ end
190
+
191
+ def find_host(instance_id, instance_name)
192
+ topology = @host_list_provider&.refresh
193
+ return nil if topology.nil? || topology.empty?
194
+
195
+ topology.find { |h| h.id == instance_id || h.host == instance_name }
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,186 @@
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 '../monitoring/monitor_state'
18
+ require_relative '../logging'
19
+ require_relative '../utils/storage/sliding_expiration_cache'
20
+ require_relative '../utils/events/data_access_event'
21
+
22
+ module AwsAdvancedRubyDriverWrapper
23
+ module Services
24
+ # Manages monitor lifecycle: registration, deduplication, expiration, and cleanup.
25
+ # Monitors are grouped by type (symbol) and keyed within each type.
26
+ # Subscribes to DataAccessEvent to extend monitor TTLs.
27
+ class MonitorService
28
+ include Logging
29
+
30
+ CLEANUP_INTERVAL_SEC = 60.0
31
+
32
+ # Internal container grouping a cache for a monitor type.
33
+ CacheContainer = Data.define(:cache, :produced_data_type)
34
+
35
+ # @param event_publisher [#subscribe] the event publisher to subscribe to.
36
+ def initialize(event_publisher:)
37
+ @caches = {}
38
+ @lock = Mutex.new
39
+ @running = true
40
+ @cleanup_thread = start_cleanup_thread
41
+ event_publisher.subscribe(
42
+ self,
43
+ Set[Utils::Events::DataAccessEvent]
44
+ )
45
+ end
46
+
47
+ # Registers a monitor type. No-op if already registered.
48
+ # @param monitor_type [Symbol] identifier for the monitor type.
49
+ # @param expiration_timeout_sec [Numeric] how long an unused monitor lives before expiring.
50
+ # @param produced_data_type [Symbol, nil] the data type this monitor produces (for DataAccessEvent linking).
51
+ def register_type(monitor_type, expiration_timeout_sec:, produced_data_type: nil)
52
+ @lock.synchronize do
53
+ return if @caches.key?(monitor_type)
54
+
55
+ cache = Utils::Storage::SlidingExpirationCache.new(ttl: expiration_timeout_sec)
56
+ @caches[monitor_type] = CacheContainer.new(cache:, produced_data_type:)
57
+ end
58
+ end
59
+
60
+ # Returns or creates a monitor for the given type and key.
61
+ # If the monitor already exists, extends its expiration.
62
+ # @param monitor_type [Symbol] the registered monitor type.
63
+ # @param key [Object] unique key for this monitor instance.
64
+ # @param service_container [Object] passed to the initializer block.
65
+ # @yield [service_container] block to create the monitor if absent.
66
+ # @return [Object] the monitor instance.
67
+ def run_if_absent(monitor_type, key, service_container)
68
+ container = @lock.synchronize { @caches[monitor_type] }
69
+ raise ArgumentError, "Monitor type not registered: #{monitor_type}" unless container
70
+
71
+ container.cache.compute_if_absent(key) do
72
+ monitor = yield(service_container)
73
+ monitor.start
74
+ monitor
75
+ end
76
+ end
77
+
78
+ # Retrieves a monitor by type and key. Returns nil if absent or expired.
79
+ # @param monitor_type [Symbol] the monitor type.
80
+ # @param key [Object] the monitor key.
81
+ # @return [Object, nil]
82
+ def get(monitor_type, key)
83
+ container = @lock.synchronize { @caches[monitor_type] }
84
+ container&.cache&.get(key)
85
+ end
86
+
87
+ # Removes a monitor without stopping it.
88
+ # @param monitor_type [Symbol] the monitor type.
89
+ # @param key [Object] the monitor key.
90
+ # @return [Object, nil] the removed monitor.
91
+ def remove(monitor_type, key)
92
+ container = @lock.synchronize { @caches[monitor_type] }
93
+ container&.cache&.remove(key)
94
+ end
95
+
96
+ # Stops and removes a monitor.
97
+ # @param monitor_type [Symbol] the monitor type.
98
+ # @param key [Object] the monitor key.
99
+ def stop_and_remove(monitor_type, key)
100
+ monitor = remove(monitor_type, key)
101
+ monitor&.stop
102
+ end
103
+
104
+ # Stops and removes all monitors across all types.
105
+ def stop_and_remove_all
106
+ @lock.synchronize { @caches.values }.each do |container|
107
+ container.cache.entries.each_key do |key|
108
+ monitor = container.cache.remove(key)
109
+ monitor&.stop
110
+ end
111
+ end
112
+ end
113
+
114
+ def shutdown(grace_period:)
115
+ @running = false
116
+ begin
117
+ @cleanup_thread&.wakeup
118
+ rescue ThreadError
119
+ nil
120
+ end
121
+ @cleanup_thread&.join([grace_period, 5].min)
122
+ stop_and_remove_all
123
+ end
124
+
125
+ # Processes events from the event publisher.
126
+ # @param event [Event] the event to process.
127
+ def process_event(event)
128
+ return unless event == Utils::Events::DataAccessEvent
129
+
130
+ handle_data_access_event(event)
131
+ end
132
+
133
+ private
134
+
135
+ def handle_data_access_event(event)
136
+ @lock.synchronize { @caches.values }.each do |container|
137
+ next unless container.produced_data_type == event.data_type
138
+
139
+ container.cache.extend_expiration(event.key)
140
+ end
141
+ end
142
+
143
+ def start_cleanup_thread
144
+ thread = Thread.new do
145
+ while @running
146
+ sleep(CLEANUP_INTERVAL_SEC)
147
+ run_cleanup
148
+ end
149
+ end
150
+ thread.name = 'monitor-service-cleanup'
151
+ thread
152
+ end
153
+
154
+ def run_cleanup
155
+ @lock.synchronize { @caches.values }.each do |container|
156
+ cleanup_container(container)
157
+ end
158
+ rescue StandardError
159
+ # Cleanup must not crash the thread.
160
+ end
161
+
162
+ def cleanup_container(container)
163
+ container.cache.entries.each_key do |key|
164
+ # Remove stopped monitors
165
+ removed = container.cache.remove_if(key) { |m| m.state == Monitoring::MonitorState::STOPPED }
166
+ next if removed
167
+
168
+ # Stop and remove errored monitors
169
+ removed = container.cache.remove_if(key) { |m| m.state == Monitoring::MonitorState::ERROR }
170
+ if removed
171
+ logger.debug("Removed monitor in error state: #{key}")
172
+ removed.stop
173
+ next
174
+ end
175
+
176
+ # Remove expired monitors that can be disposed
177
+ removed = container.cache.remove_if_expired(key)
178
+ if removed
179
+ logger.debug("Removed expired monitor: #{key}")
180
+ removed.stop
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end