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,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License").
7
+ # You may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative '../../monitoring/monitor'
19
+ require_relative '../../logging'
20
+ require_relative 'info'
21
+ require_relative 'member_list_type'
22
+ require 'concurrent'
23
+
24
+ module AwsAdvancedRubyDriverWrapper
25
+ module Plugins
26
+ module CustomEndpoint
27
+ class CustomEndpointMonitor < Monitoring::Monitor
28
+ include Logging
29
+
30
+ TERMINATION_TIMEOUT_SEC = 30.0
31
+ ENDPOINT_INFO_EXPIRATION_SEC = 300.0
32
+ UNAUTHORIZED_SLEEP_SEC = 300.0
33
+ ENDPOINT_INFO_CACHE_NAME = :custom_endpoint
34
+ ALLOWED_BLOCKED_CACHE_NAME = :custom_endpoint_allowed_blocked
35
+
36
+ def initialize(
37
+ service_container,
38
+ custom_endpoint_host,
39
+ endpoint_id,
40
+ region,
41
+ refresh_rate_sec,
42
+ refresh_rate_backoff_factor,
43
+ max_refresh_rate_sec,
44
+ rds_client_func: ->(_, region) { Aws::RDS::Client.new(region: region) }
45
+ )
46
+ super(termination_timeout_sec: TERMINATION_TIMEOUT_SEC)
47
+
48
+ @service_container = service_container
49
+ @custom_endpoint_host = custom_endpoint_host
50
+ @endpoint_id = endpoint_id
51
+ @min_refresh_rate_sec = refresh_rate_sec
52
+ @refresh_rate_sec = refresh_rate_sec
53
+ @refresh_rate_backoff_factor = refresh_rate_backoff_factor
54
+ @max_refresh_rate_sec = max_refresh_rate_sec
55
+ @rds_client = rds_client_func.call(custom_endpoint_host, region)
56
+
57
+ @refresh_mutex = Mutex.new
58
+ @refresh_cv = ConditionVariable.new
59
+ @refresh_required = false
60
+ @connection_issue = false
61
+ end
62
+
63
+ def endpoint_info?
64
+ info = cached_info
65
+ request_refresh if info.nil? && @refresh_mutex.synchronize { !@refresh_required && !@connection_issue }
66
+ !info.nil?
67
+ end
68
+
69
+ # Waits up to timeout_sec for the monitor to place endpoint info in the cache. Polls rather than
70
+ # waiting on a one-shot signal: #endpoint_info? re-checks the cache and re-requests a refresh
71
+ # (waking the monitor) on each pass, so a transient empty cache - e.g. an entry that aged out
72
+ # between monitor iterations - is repopulated within the window instead of failing immediately.
73
+ def wait_for_info?(timeout_sec)
74
+ deadline = monotonic_time + timeout_sec
75
+ loop do
76
+ return true if endpoint_info?
77
+ return false if monotonic_time >= deadline
78
+
79
+ sleep(0.1)
80
+ end
81
+ end
82
+
83
+ def request_endpoint_info_update
84
+ return if @refresh_mutex.synchronize { @connection_issue }
85
+
86
+ request_refresh
87
+ end
88
+
89
+ def close
90
+ remove_cached_info
91
+ end
92
+
93
+ def self.clear_cache(storage_service)
94
+ storage_service.clear(ENDPOINT_INFO_CACHE_NAME)
95
+ storage_service.clear(ALLOWED_BLOCKED_CACHE_NAME)
96
+ end
97
+
98
+ private
99
+
100
+ def monitor
101
+ logger.debug("[#{@endpoint_id}] Started custom endpoint monitor for #{@custom_endpoint_host.url}")
102
+
103
+ until stopped?
104
+ update_activity
105
+ run_monitor_iteration
106
+ end
107
+ rescue StandardError => e
108
+ logger.error("[#{@endpoint_id}] Unexpected error in custom endpoint monitor: #{e.message}")
109
+ ensure
110
+ remove_cached_info
111
+ logger.debug("[#{@endpoint_id}] Stopped custom endpoint monitor for #{@custom_endpoint_host.url}")
112
+ end
113
+
114
+ def run_monitor_iteration
115
+ start = monotonic_time
116
+
117
+ endpoints = fetch_endpoints
118
+ return unless endpoints
119
+
120
+ @refresh_mutex.synchronize do
121
+ @connection_issue = false
122
+ @refresh_required = false
123
+ end
124
+
125
+ unless valid_endpoint_count?(endpoints)
126
+ sleep_ignoring_refresh_requests(@refresh_rate_sec)
127
+ return
128
+ end
129
+
130
+ endpoint_info = Info.from_db_cluster_endpoint(endpoints.first)
131
+ process_endpoint_info(endpoint_info, start)
132
+ rescue Aws::RDS::Errors::ServiceError => e
133
+ handle_rds_error(e)
134
+ rescue StandardError => e
135
+ logger.error("[#{@endpoint_id}] Exception monitoring #{@custom_endpoint_host.url}: #{e.message}")
136
+ sleep_ignoring_refresh_requests(@refresh_rate_sec)
137
+ end
138
+
139
+ def fetch_endpoints
140
+ response = @rds_client.describe_db_cluster_endpoints(
141
+ db_cluster_endpoint_identifier: @endpoint_id,
142
+ filters: [{ name: 'db-cluster-endpoint-type', values: ['custom'] }]
143
+ )
144
+ response.db_cluster_endpoints
145
+ end
146
+
147
+ def valid_endpoint_count?(endpoints)
148
+ return true if endpoints.size == 1
149
+
150
+ logger.warn("[#{@endpoint_id}] Expected 1 endpoint, got #{endpoints.size}: #{endpoints.map(&:endpoint)}")
151
+ false
152
+ end
153
+
154
+ def process_endpoint_info(endpoint_info, start)
155
+ if cached_info == endpoint_info
156
+ elapsed = monotonic_time - start
157
+ interruptible_sleep([0, @refresh_rate_sec - elapsed].max)
158
+ return
159
+ end
160
+
161
+ logger.debug("[#{@endpoint_id}] Custom endpoint info changed: #{endpoint_info}")
162
+ cache_allowed_blocked(endpoint_info)
163
+ cache_info(endpoint_info)
164
+ speedup_refresh_rate
165
+
166
+ elapsed = monotonic_time - start
167
+ interruptible_sleep([0, @refresh_rate_sec - elapsed].max)
168
+ end
169
+
170
+ def cache_allowed_blocked(endpoint_info)
171
+ value = if endpoint_info.member_list_type == MemberListType::STATIC_LIST
172
+ { allowed: endpoint_info.static_members, blocked: nil, required_role: endpoint_info.required_role }
173
+ else
174
+ { allowed: nil, blocked: endpoint_info.excluded_members, required_role: endpoint_info.required_role }
175
+ end
176
+ storage_service.set(ALLOWED_BLOCKED_CACHE_NAME, @custom_endpoint_host.url, value)
177
+ end
178
+
179
+ def handle_rds_error(error)
180
+ logger.error("[#{@endpoint_id}] RDS error for #{@custom_endpoint_host.url}: #{error.message}")
181
+
182
+ if throttling_error?(error)
183
+ slowdown_refresh_rate
184
+ sleep_ignoring_refresh_requests(@refresh_rate_sec)
185
+ elsif unauthorized_error?(error)
186
+ sleep_ignoring_refresh_requests(UNAUTHORIZED_SLEEP_SEC)
187
+ else
188
+ sleep_ignoring_refresh_requests(@refresh_rate_sec)
189
+ end
190
+ end
191
+
192
+ def throttling_error?(error)
193
+ error.context&.http_response&.status_code == 429 ||
194
+ error.code == 'ThrottlingException' ||
195
+ error.code == 'Throttling'
196
+ end
197
+
198
+ def unauthorized_error?(error)
199
+ [401, 403].include?(error.context&.http_response&.status_code)
200
+ end
201
+
202
+ def speedup_refresh_rate
203
+ return unless @refresh_rate_sec > @min_refresh_rate_sec
204
+
205
+ @refresh_rate_sec = [@refresh_rate_sec / @refresh_rate_backoff_factor, @min_refresh_rate_sec].max
206
+ end
207
+
208
+ def slowdown_refresh_rate
209
+ return unless @refresh_rate_sec < @max_refresh_rate_sec
210
+
211
+ @refresh_rate_sec = [@refresh_rate_sec * @refresh_rate_backoff_factor, @max_refresh_rate_sec].min
212
+ end
213
+
214
+ def request_refresh
215
+ @refresh_mutex.synchronize do
216
+ @refresh_required = true
217
+ @refresh_cv.broadcast
218
+ end
219
+ end
220
+
221
+ # Wakes early if a refresh is requested or the monitor is stopped.
222
+ def interruptible_sleep(duration_sec)
223
+ end_time = monotonic_time + duration_sec
224
+ wait_sec = [0.5, duration_sec].min
225
+
226
+ @refresh_mutex.synchronize do
227
+ @refresh_cv.wait(@refresh_mutex, wait_sec) until @refresh_required || monotonic_time >= end_time || stopped?
228
+ end
229
+ end
230
+
231
+ # Sleeps for the full duration, ignoring refresh requests. Used on error/backoff paths
232
+ # to prevent connections from bypassing throttling backoff.
233
+ def sleep_ignoring_refresh_requests(duration_sec)
234
+ end_time = monotonic_time + duration_sec
235
+ until stopped?
236
+ remaining = end_time - monotonic_time
237
+ break if remaining <= 0
238
+
239
+ sleep([0.5, remaining].min)
240
+ end
241
+ end
242
+
243
+ def cached_info
244
+ storage_service.get(ENDPOINT_INFO_CACHE_NAME, @custom_endpoint_host.url, register_access: false)
245
+ end
246
+
247
+ def cache_info(info)
248
+ storage_service.set(ENDPOINT_INFO_CACHE_NAME, @custom_endpoint_host.url, info)
249
+ end
250
+
251
+ def remove_cached_info
252
+ storage_service.remove(ENDPOINT_INFO_CACHE_NAME, @custom_endpoint_host.url)
253
+ storage_service.remove(ALLOWED_BLOCKED_CACHE_NAME, @custom_endpoint_host.url)
254
+ end
255
+
256
+ def storage_service
257
+ @service_container.storage_service
258
+ end
259
+
260
+ def monotonic_time
261
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'concurrent'
18
+ require_relative '../../errors'
19
+ require_relative '../../logging'
20
+ require_relative '../../property_definition'
21
+ require_relative '../../ruby_method'
22
+ require_relative '../../utils/rds_utils'
23
+ require_relative 'custom_endpoint_monitor'
24
+
25
+ module AwsAdvancedRubyDriverWrapper
26
+ module Plugins
27
+ module CustomEndpoint
28
+ class CustomEndpointPlugin
29
+ include Logging
30
+
31
+ MONITOR_TYPE = :custom_endpoint
32
+
33
+ attr_reader :subscribed_methods
34
+
35
+ def initialize(service_container, props = ::Concurrent::Map.new)
36
+ ensure_aws_sdk!
37
+ @service_container = service_container
38
+ @props = props
39
+ @should_wait_for_info = PropertyDefinition::WAIT_FOR_CUSTOM_ENDPOINT_INFO.get_bool(props)
40
+ @wait_timeout_sec = PropertyDefinition::WAIT_FOR_CUSTOM_ENDPOINT_INFO_TIMEOUT_SEC.get_float(props)
41
+
42
+ @custom_endpoint_host = nil
43
+ @endpoint_id = nil
44
+ @region = nil
45
+
46
+ monitor_expiration_sec = PropertyDefinition::CUSTOM_ENDPOINT_MONITOR_EXPIRATION_SEC.get_float(props)
47
+ service_container.monitor_service.register_type(
48
+ MONITOR_TYPE,
49
+ expiration_timeout_sec: monitor_expiration_sec,
50
+ produced_data_type: CustomEndpointMonitor::ENDPOINT_INFO_CACHE_NAME
51
+ )
52
+ service_container.storage_service.register(
53
+ CustomEndpointMonitor::ENDPOINT_INFO_CACHE_NAME,
54
+ ttl: CustomEndpointMonitor::ENDPOINT_INFO_EXPIRATION_SEC
55
+ )
56
+ service_container.storage_service.register(
57
+ CustomEndpointMonitor::ALLOWED_BLOCKED_CACHE_NAME,
58
+ ttl: CustomEndpointMonitor::ENDPOINT_INFO_EXPIRATION_SEC
59
+ )
60
+
61
+ network_methods = service_container.dialect_service.driver_dialect.network_bound_methods
62
+ @subscribed_methods = (Set['connect'] | network_methods).freeze
63
+ end
64
+
65
+ def connect(host_info, _, _, pipeline_callable)
66
+ return pipeline_callable.call unless Utils::RdsUtils.rds_custom_cluster_dns?(host_info.host)
67
+
68
+ logger.debug("CustomEndpointPlugin: connection request to custom endpoint '#{host_info.url}'")
69
+ init_endpoint_state!(host_info) if @custom_endpoint_host.nil?
70
+ monitor = create_monitor_if_absent(@props)
71
+ wait_for_endpoint_info(monitor) if @should_wait_for_info
72
+ pipeline_callable.call
73
+ end
74
+
75
+ CLOSE_METHODS = Set[
76
+ RubyMethod::CONNECTION_CLOSE.name,
77
+ RubyMethod::CONNECTION_RESET.name
78
+ ].freeze
79
+
80
+ def execute(method_name, pipeline_callable, *_, **_)
81
+ return pipeline_callable.call if @custom_endpoint_host.nil?
82
+ return pipeline_callable.call if CLOSE_METHODS.include?(method_name.to_s)
83
+
84
+ monitor = create_monitor_if_absent(@props)
85
+ wait_for_endpoint_info(monitor) if @should_wait_for_info
86
+ pipeline_callable.call
87
+ end
88
+
89
+ def self.clear_cache(storage_service)
90
+ CustomEndpointMonitor.clear_cache(storage_service)
91
+ end
92
+
93
+ private
94
+
95
+ def init_endpoint_state!(host_info)
96
+ @custom_endpoint_host = host_info
97
+
98
+ endpoint_id = Utils::RdsUtils.rds_cluster_id(host_info.host)
99
+ if endpoint_id.nil? || endpoint_id.empty?
100
+ raise Errors::AwsError, "CustomEndpointPlugin: unable to parse endpoint identifier from '#{host_info.host}'"
101
+ end
102
+
103
+ @endpoint_id = endpoint_id
104
+ @region = resolve_region!(host_info.host)
105
+ end
106
+
107
+ def resolve_region!(host)
108
+ region = PropertyDefinition::CUSTOM_ENDPOINT_REGION.get(@props) || Utils::RdsUtils.rds_region(host)
109
+ unless region
110
+ raise Errors::AwsError,
111
+ "CustomEndpointPlugin: unable to determine region for '#{host}'. " \
112
+ "Set the '#{PropertyDefinition::CUSTOM_ENDPOINT_REGION.name}' property explicitly."
113
+ end
114
+
115
+ region
116
+ end
117
+
118
+ def create_monitor_if_absent(props)
119
+ @service_container.monitor_service.run_if_absent(
120
+ MONITOR_TYPE,
121
+ @custom_endpoint_host.url,
122
+ @service_container
123
+ ) do |service_container|
124
+ CustomEndpointMonitor.new(
125
+ service_container,
126
+ @custom_endpoint_host,
127
+ @endpoint_id,
128
+ @region,
129
+ PropertyDefinition::CUSTOM_ENDPOINT_INFO_REFRESH_RATE_SEC.get_float(props),
130
+ PropertyDefinition::CUSTOM_ENDPOINT_INFO_REFRESH_RATE_BACKOFF_FACTOR.get_int(props),
131
+ PropertyDefinition::CUSTOM_ENDPOINT_INFO_MAX_REFRESH_RATE_SEC.get_float(props)
132
+ )
133
+ end
134
+ end
135
+
136
+ def wait_for_endpoint_info(monitor)
137
+ return if monitor.endpoint_info?
138
+
139
+ monitor.request_endpoint_info_update
140
+ logger.debug("CustomEndpointPlugin: waiting up to #{@wait_timeout_sec}s for endpoint info on #{@custom_endpoint_host.url}")
141
+
142
+ return if monitor.wait_for_info?(@wait_timeout_sec)
143
+
144
+ raise Errors::AwsError,
145
+ "CustomEndpointPlugin: timed out after #{@wait_timeout_sec}s " \
146
+ "waiting for custom endpoint info for '#{@custom_endpoint_host.url}'"
147
+ end
148
+
149
+ def ensure_aws_sdk!
150
+ require 'aws-sdk-rds'
151
+ rescue LoadError
152
+ raise LoadError,
153
+ "The custom endpoint plugin requires 'aws-sdk-rds'. Add it to your Gemfile: gem 'aws-sdk-rds'"
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,111 @@
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 'member_list_type'
18
+ require_relative 'role'
19
+
20
+ module AwsAdvancedRubyDriverWrapper
21
+ module Plugins
22
+ module CustomEndpoint
23
+ # Represents custom endpoint information for a given custom endpoint.
24
+ class Info
25
+ attr_reader :endpoint_identifier, :cluster_identifier, :url, :role, :member_list_type, :members
26
+
27
+ # @param endpoint_identifier [String] The endpoint identifier for the custom endpoint. For example, if the
28
+ # custom endpoint URL is "my-custom-endpoint.cluster-custom-XYZ.us-east-1.rds.amazonaws.com", the endpoint
29
+ # identifier is "my-custom-endpoint".
30
+ # @param cluster_identifier [String] The cluster identifier for the cluster that the custom endpoint belongs to.
31
+ # @param url [String] The URL for the custom endpoint.
32
+ # @param role [Symbol] The role of the custom endpoint.
33
+ # @param members [Enumerable<String>] The instance IDs for the hosts in the custom endpoint.
34
+ # @param member_list_type [Symbol] The list type for +members+.
35
+ def initialize(endpoint_identifier:,
36
+ cluster_identifier:,
37
+ url:,
38
+ role:,
39
+ members:,
40
+ member_list_type:)
41
+ @endpoint_identifier = endpoint_identifier
42
+ @cluster_identifier = cluster_identifier
43
+ @url = url
44
+ @role = role
45
+ @members = members.to_set.freeze
46
+ @member_list_type = member_list_type
47
+ end
48
+
49
+ def self.from_db_cluster_endpoint(response)
50
+ static = response.static_members&.any?
51
+ members = static ? response.static_members : response.excluded_members
52
+ member_list_type = static ? MemberListType::STATIC_LIST : MemberListType::EXCLUSION_LIST
53
+
54
+ new(
55
+ endpoint_identifier: response.db_cluster_endpoint_identifier,
56
+ cluster_identifier: response.db_cluster_identifier,
57
+ url: response.endpoint,
58
+ role: Role.parse(response.custom_endpoint_type),
59
+ members: members,
60
+ member_list_type: member_list_type
61
+ )
62
+ end
63
+
64
+ # Evaluates whether instances in the custom endpoint must match a particular role according to the custom
65
+ # endpoint properties. Note that custom clusters with static member lists always route to all static members,
66
+ # even if the member is a writer and the custom endpoint is of type READER, so there are never role
67
+ # requirements for static list custom clusters.
68
+ # @return [Symbol, nil] the required role of instances in the custom endpoint, or nil if there is no strict
69
+ # role requirement.
70
+ def required_role
71
+ :reader if @member_list_type == MemberListType::EXCLUSION_LIST && @role == Role::READER
72
+ end
73
+
74
+ # Gets the static members of the custom endpoint. If the custom endpoint member list type is an exclusion
75
+ # list, returns nil.
76
+ # @return [Set<String>, nil]
77
+ def static_members
78
+ @members if @member_list_type == MemberListType::STATIC_LIST
79
+ end
80
+
81
+ # Gets the excluded members of the custom endpoint. If the custom endpoint member list type is a static
82
+ # list, returns nil.
83
+ # @return [Set<String>, nil]
84
+ def excluded_members
85
+ @members if @member_list_type == MemberListType::EXCLUSION_LIST
86
+ end
87
+
88
+ def ==(other)
89
+ other.is_a?(Info) &&
90
+ endpoint_identifier == other.endpoint_identifier &&
91
+ cluster_identifier == other.cluster_identifier &&
92
+ url == other.url &&
93
+ role == other.role &&
94
+ member_list_type == other.member_list_type &&
95
+ members == other.members
96
+ end
97
+
98
+ alias eql? ==
99
+
100
+ def hash
101
+ [@endpoint_identifier, @cluster_identifier, @url, @role, @member_list_type, @members].hash
102
+ end
103
+
104
+ def to_s
105
+ "Info[url=#{@url}, cluster=#{@cluster_identifier}, role=#{@role}, " \
106
+ "member_list_type=#{@member_list_type}, members=#{@members}]"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,31 @@
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
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module CustomEndpoint
20
+ # Represents the member list type of a custom endpoint.
21
+ # Used with a member list to determine which instances are included or excluded.
22
+ module MemberListType
23
+ # Only the listed instances are included. New cluster instances are NOT auto-added.
24
+ STATIC_LIST = :static_list
25
+
26
+ # The listed instances are excluded. New cluster instances ARE auto-added.
27
+ EXCLUSION_LIST = :exclusion_list
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
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
+ module AwsAdvancedRubyDriverWrapper
18
+ module Plugins
19
+ module CustomEndpoint
20
+ # Represents the possible roles of instances specified by a custom endpoint.
21
+ module Role
22
+ # Instances may be either a writer or a reader.
23
+ ANY = :any
24
+
25
+ # Instance is always the writer.
26
+ WRITER = :writer
27
+
28
+ # Instances are always readers.
29
+ READER = :reader
30
+
31
+ ROLE_MAPPING = {
32
+ 'ANY' => ANY,
33
+ 'WRITER' => WRITER,
34
+ 'READER' => READER
35
+ }.freeze
36
+
37
+ def self.parse(value)
38
+ raise ArgumentError, 'Role value is blank' if value.nil? || value.strip.empty?
39
+
40
+ ROLE_MAPPING.fetch(value.upcase) { raise ArgumentError, "Unknown role: #{value}" }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end