aws_advanced_ruby_driver_wrapper 1.0.1 → 1.1.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +115 -6
  4. data/THIRD-PARTY-LICENSES +70 -11
  5. data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
  6. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +17 -3
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
  9. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  11. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  17. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  21. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
  22. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  24. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
  49. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  50. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  52. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  53. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
  54. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  55. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  57. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  59. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  60. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  62. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  79. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  80. data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
  81. metadata +54 -2
@@ -20,6 +20,7 @@ require_relative '../aws_advanced_ruby_driver_wrapper'
20
20
  require_relative 'utils/connection_config_parser'
21
21
  require_relative 'utils/sql_encoding'
22
22
  require_relative 'services/service_utility'
23
+ require_relative 'utils/telemetry/telemetry_factory'
23
24
  require_relative 'ruby_method'
24
25
  require_relative 'errors'
25
26
 
@@ -34,13 +35,16 @@ module AwsAdvancedRubyDriverWrapper
34
35
  def initialize(**)
35
36
  ensure_mysql2!
36
37
  config = Utils::ConnectionConfigParser.parse(:mysql2, **)
37
- @service_container = Services::ServiceUtility.create_standard_container(config)
38
- @service_container.host_service.refresh_host_list
39
- @async_conn = nil
40
- @async_sql = nil
41
- @last_sql = nil
42
- conn_service = @service_container.connection_service
43
- @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
38
+ telemetry_factory = Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
39
+ telemetry_factory.in_context("#{self.class.name}.new", Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do
40
+ @service_container = Services::ServiceUtility.create_standard_container(config, telemetry_factory:)
41
+ @service_container.host_service.refresh_host_list
42
+ @async_conn = nil
43
+ @async_sql = nil
44
+ @last_sql = nil
45
+ conn_service = @service_container.connection_service
46
+ @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
47
+ end
44
48
  end
45
49
 
46
50
  # Explicitly define critical methods (bypass method_missing to avoid method_missing overhead)
@@ -53,6 +57,7 @@ module AwsAdvancedRubyDriverWrapper
53
57
  # What is remembered is the copy of the SQL that plugins inspect, made once here rather than on
54
58
  # every call that is handed it later. The driver is sent the SQL as the application wrote it.
55
59
  def query(sql, options = {})
60
+ ensure_connected!
56
61
  inspected = Utils::SqlEncoding.inspectable(sql)
57
62
  result = pm.execute(RubyMethod::CONNECTION_QUERY, current_conn, ->(*a) { current_conn.query(*a) }, sql, options,
58
63
  sql: inspected)
@@ -76,10 +81,14 @@ module AwsAdvancedRubyDriverWrapper
76
81
  end
77
82
 
78
83
  def ping
84
+ return false if current_conn.nil?
85
+
79
86
  pm.execute(RubyMethod::CONNECTION_PING, current_conn, -> { current_conn.ping })
80
87
  end
81
88
 
82
89
  def close
90
+ return if current_conn.nil?
91
+
83
92
  pm.execute(RubyMethod::CONNECTION_CLOSE, current_conn, -> { current_conn.close })
84
93
  end
85
94
 
@@ -143,8 +152,7 @@ module AwsAdvancedRubyDriverWrapper
143
152
  BOUNDED_TO_ASYNC = Set[:abandon_results!].freeze
144
153
 
145
154
  def method_missing(method_name, *args, **kwargs, &)
146
- conn = current_conn
147
- raise NoMethodError, 'Connection not initialized' if conn.nil?
155
+ conn = ensure_connected!
148
156
  raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
149
157
 
150
158
  method_key = "connection.#{method_name}"
@@ -197,6 +205,16 @@ module AwsAdvancedRubyDriverWrapper
197
205
  @service_container.connection_service.current_connection
198
206
  end
199
207
 
208
+ # Guards the entry points that reach for the current connection directly (the query path and
209
+ # method_missing). Without a live connection AR expects the driver's own "not connected" error,
210
+ # which it translates to a connection error; a bare NoMethodError on nil would not be recognized.
211
+ def ensure_connected!
212
+ conn = current_conn
213
+ raise Mysql2::Error::ConnectionError, 'MySQL client is not connected' if conn.nil?
214
+
215
+ conn
216
+ end
217
+
200
218
  def pm
201
219
  @service_container.plugin_manager
202
220
  end
@@ -19,6 +19,7 @@ require_relative '../../logging'
19
19
  require_relative '../../ruby_method'
20
20
  require_relative '../../property_definition'
21
21
  require_relative '../../utils/rds_utils'
22
+ require_relative 'drop_blue_connections_marker'
22
23
  require_relative 'phase'
23
24
  require_relative 'role'
24
25
  require_relative 'status'
@@ -32,6 +33,14 @@ module AwsAdvancedRubyDriverWrapper
32
33
 
33
34
  BLUE_GREEN_NAME = :blue_green
34
35
 
36
+ # Storage partition for the drop-blue-connections marker, kept separate from BLUE_GREEN_NAME so
37
+ # it can outlive the status cache's TTL. The marker is stored once per switchover and never
38
+ # re-stamped, so a connection that sits idle longer than the status cache's TTL must still find
39
+ # the marker on its next use. A long TTL is safe: a marker only matches connections opened before
40
+ # its own switchover, and a later switchover overwrites it.
41
+ DROP_MARKER_NAME = :blue_green_drop_marker
42
+ DROP_MARKER_TTL_SEC = 86_400
43
+
35
44
  # When present in wrapper_props, internal_connect calls bypass BG routing.
36
45
  # Used by BG monitoring connections to avoid being intercepted by their own plugin.
37
46
  BG_SKIP_ROUTING_KEY = :'bg.skip_routing'
@@ -43,6 +52,13 @@ module AwsAdvancedRubyDriverWrapper
43
52
 
44
53
  PROVIDERS = Concurrent::Map.new
45
54
 
55
+ # Storage key, within the DROP_MARKER_NAME cache, under which the drop-blue-connections marker
56
+ # for a deployment is kept. DROP_MARKER_NAME is a separate partition from the status cache, so
57
+ # the marker survives both the status cache's TTL and the provider's reset_context_when_completed.
58
+ def self.drop_marker_key(bgd_id)
59
+ "#{bgd_id}::drop_blue_connections_marker"
60
+ end
61
+
46
62
  def initialize(service_container, props = ::Concurrent::Map.new)
47
63
  @service_container = service_container
48
64
  @wrapper_props = props
@@ -52,7 +68,17 @@ module AwsAdvancedRubyDriverWrapper
52
68
  @start_time_ns = Concurrent::AtomicReference.new(0)
53
69
  @end_time_ns = Concurrent::AtomicReference.new(0)
54
70
 
71
+ # Per-connection drop-blue-connections state. Each wrapper client has its own plugin instance,
72
+ # so these describe this connection: when it was opened, the host it was opened to, and the
73
+ # identity of the last marker it was checked against.
74
+ @drop_blue_connections = PropertyDefinition::BG_DROP_BLUE_CONNECTIONS.get_bool(props)
75
+ @connection_open_time_ns = nil
76
+ @connect_time_host = nil
77
+ @checked_drop_marker_id = nil
78
+ @pending_stale_marker_id = nil
79
+
55
80
  service_container.storage_service.register(BLUE_GREEN_NAME, ttl: 3600)
81
+ service_container.storage_service.register(DROP_MARKER_NAME, ttl: DROP_MARKER_TTL_SEC)
56
82
 
57
83
  network_methods = service_container.dialect_service.driver_dialect.network_bound_methods
58
84
  @subscribed_methods = (Set['connect', 'internal_connect'] | network_methods).freeze
@@ -61,7 +87,12 @@ module AwsAdvancedRubyDriverWrapper
61
87
  attr_reader :subscribed_methods
62
88
 
63
89
  def connect(host_info, driver_props, is_initial_connection, pipeline_callable)
64
- route_connect(host_info, driver_props, is_initial_connection, pipeline_callable, is_internal: false)
90
+ conn = route_connect(host_info, driver_props, is_initial_connection, pipeline_callable, is_internal: false)
91
+ # Only this client's own connect describes its connection. Internal connects (topology
92
+ # monitoring, KMS metadata reads) run through this same plugin instance but open separate
93
+ # connections, so they must not overwrite this connection's open time, host, or marker latches.
94
+ record_connection_open(host_info)
95
+ conn
65
96
  end
66
97
 
67
98
  def internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, pipeline_callable)
@@ -79,7 +110,21 @@ module AwsAdvancedRubyDriverWrapper
79
110
 
80
111
  return pipeline_callable.call if CLOSING_METHODS.include?(method_name)
81
112
 
113
+ # Read the status before the stale check. The provider stores the active drop marker before
114
+ # it writes the POST/COMPLETED status, so reading status first closes the race where a stale
115
+ # check sees a still-pending marker and the COMPLETED early return below then skips re-checking
116
+ # the now-active marker: if the switchover has completed, the check below sees the active marker.
82
117
  @bg_status = storage_service.get(BLUE_GREEN_NAME, @bgd_id)
118
+
119
+ # Invalidate a connection opened to the old blue environment before a confirmed switchover,
120
+ # so the failover plugin reconnects through BG routing instead of serving a stale read. This
121
+ # runs before the NOT_CREATED/COMPLETED early return below, because after the switchover the
122
+ # status is COMPLETED (then NOT_CREATED) and would otherwise skip the check entirely.
123
+ if stale_blue_connection?
124
+ invalidate_stale_blue_connection
125
+ return pipeline_callable.call
126
+ end
127
+
83
128
  if @bg_status.nil? || @bg_status.current_phase == Phase::NOT_CREATED || @bg_status.current_phase == Phase::COMPLETED
84
129
  return pipeline_callable.call
85
130
  end
@@ -95,20 +140,24 @@ module AwsAdvancedRubyDriverWrapper
95
140
  result = nil
96
141
 
97
142
  while routing && result.nil?
98
- result = routing.apply(method_name, @wrapper_props, storage_service)
143
+ result = routing.apply(method_name, @wrapper_props, storage_service, telemetry_factory: @service_container.telemetry_factory)
99
144
 
100
145
  next unless result.nil?
101
146
 
102
147
  @bg_status = storage_service.get(BLUE_GREEN_NAME, @bgd_id)
103
- if @bg_status.nil?
104
- @end_time_ns.set(nano_time)
105
- return pipeline_callable.call
106
- end
148
+ break if @bg_status.nil?
149
+
107
150
  routing = @bg_status.execute_routing.find { |r| r.match?(current_host, host_role) }
108
151
  end
109
152
 
110
153
  @end_time_ns.set(nano_time)
111
- result.nil? ? pipeline_callable.call : result
154
+ return result unless result.nil?
155
+
156
+ # The call may have been held through the switchover (e.g. suspended during IN_PROGRESS and
157
+ # released at POST, which has no execute routing). Re-check whether this connection is now a
158
+ # stale blue connection before running it, so a held call is not run against retired blue.
159
+ invalidate_stale_blue_connection if stale_blue_connection?
160
+ pipeline_callable.call
112
161
  ensure
113
162
  @end_time_ns.set(nano_time) if @start_time_ns.get.positive? && @end_time_ns.get.zero?
114
163
  end
@@ -202,6 +251,65 @@ module AwsAdvancedRubyDriverWrapper
202
251
  end
203
252
  end
204
253
 
254
+ # Remembers when this connection was opened and the host it was opened to, so a later switchover
255
+ # can tell whether it is a blue connection that predates the switchover.
256
+ def record_connection_open(host_info)
257
+ return unless @drop_blue_connections
258
+
259
+ @connection_open_time_ns = nano_time
260
+ @connect_time_host = host_info&.host&.downcase
261
+ # A (re)connect produces a fresh connection (e.g. after failover): reset the per-connection
262
+ # marker latches so this new incarnation re-evaluates the current marker from a clean slate
263
+ # instead of inheriting the retired connection's checked/pending state.
264
+ @checked_drop_marker_id = nil
265
+ @pending_stale_marker_id = nil
266
+ end
267
+
268
+ # Whether this connection should be invalidated because it was opened to the old blue environment
269
+ # before a now-confirmed switchover. The marker is read on every call so a later switchover's
270
+ # marker is noticed. A marker that does not apply to this connection is latched immediately and
271
+ # never re-examined; a marker that does apply is only latched once #invalidate_stale_blue_connection
272
+ # confirms the close, so a transient close failure leaves the connection to be invalidated again on
273
+ # its next use instead of silently serving stale reads for the rest of its life.
274
+ def stale_blue_connection?
275
+ return false unless @drop_blue_connections
276
+
277
+ marker = storage_service.get(DROP_MARKER_NAME, self.class.drop_marker_key(@bgd_id), register_access: false)
278
+ return false unless marker.is_a?(DropBlueConnectionsMarker) && marker.active?
279
+ return false if marker.id == @checked_drop_marker_id
280
+
281
+ if marker.stale?(@connect_time_host, @connection_open_time_ns)
282
+ @pending_stale_marker_id = marker.id
283
+ true
284
+ else
285
+ # This connection does not predate the switchover and never will against this marker (markers
286
+ # are immutable), so latch it now to skip re-reading the marker on every future call.
287
+ @checked_drop_marker_id = marker.id
288
+ false
289
+ end
290
+ end
291
+
292
+ # Closes the connection directly through the driver dialect rather than the pipeline's close
293
+ # method. Going through the pipeline would set the failover plugin's @closed_explicitly, which
294
+ # would stop it from reconnecting. Closing it out from under the pipeline instead lets the
295
+ # failover plugin (if enabled) see a connection closed that it did not close, fail over, and
296
+ # raise FailoverSuccessError; without failover the driver raises its closed-connection error.
297
+ def invalidate_stale_blue_connection
298
+ logger.info do
299
+ "[bgd_id: '#{@bgd_id}'] Invalidating a connection to #{@connect_time_host} from the old blue environment " \
300
+ 'retired by a Blue/Green switchover. It will fail over (or raise a closed-connection error) on this use.'
301
+ end
302
+
303
+ conn = @service_container.connection_service.current_connection
304
+ @service_container.dialect_service.driver_dialect.close_connection(conn) unless conn.nil?
305
+ # Only now that the close is confirmed do we latch the marker as handled. If the close raised
306
+ # and was swallowed below, the marker stays unlatched so the next use retries the invalidation
307
+ # rather than serving a stale read from retired blue for the rest of the connection's life.
308
+ @checked_drop_marker_id = @pending_stale_marker_id
309
+ rescue StandardError => e
310
+ logger.debug { "[bgd_id: '#{@bgd_id}'] Failed to invalidate stale blue connection: #{e.class}: #{e.message}" }
311
+ end
312
+
205
313
  def storage_service
206
314
  @service_container.storage_service
207
315
  end
@@ -0,0 +1,84 @@
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 'securerandom'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Plugins
21
+ module BlueGreen
22
+ # Records that a Blue/Green switchover is retiring the blue environment, so that connections
23
+ # opened to blue before the switchover can be invalidated the next time they are used.
24
+ #
25
+ # Ruby cannot safely abort a connection from another thread, so instead of dropping blue connections
26
+ # from the monitor thread, each connection checks this marker on its own thread the next time it is
27
+ # used. See the +bg_drop_blue_connections+ property and BlueGreenPlugin#execute.
28
+ #
29
+ # A marker is immutable. It becomes +pending+ when the switchover reaches IN_PROGRESS and is
30
+ # replaced by an +active+ copy (see #activated) once the switchover is confirmed by POST or
31
+ # COMPLETED. It is cleared on rollback.
32
+ class DropBlueConnectionsMarker
33
+ # @return [String] a unique identity for this switchover's marker, so a connection can tell
34
+ # whether it has already been checked against it.
35
+ attr_reader :id
36
+
37
+ # @return [Integer] monotonic time (nanoseconds) at which the switchover reached IN_PROGRESS.
38
+ attr_reader :switchover_time_ns
39
+
40
+ # @return [Set<String>] the blue host set: downcased cluster and instance endpoint names plus
41
+ # their pre-switchover IP addresses.
42
+ attr_reader :blue_hosts
43
+
44
+ # @param blue_hosts [Enumerable<String>] blue endpoint names and their pre-switchover IPs
45
+ # @param switchover_time_ns [Integer] monotonic time the switchover reached IN_PROGRESS
46
+ # @param id [String] unique identity for this marker
47
+ # @param active [Boolean] false while the switchover is in progress, true once it is confirmed
48
+ def initialize(blue_hosts:, switchover_time_ns:, id: SecureRandom.uuid, active: false)
49
+ @id = id
50
+ @switchover_time_ns = switchover_time_ns
51
+ @blue_hosts = blue_hosts.to_set.freeze
52
+ @active = active
53
+ freeze
54
+ end
55
+
56
+ def active?
57
+ @active
58
+ end
59
+
60
+ # @return [DropBlueConnectionsMarker] an active copy of this marker, keeping its identity.
61
+ def activated
62
+ return self if @active
63
+
64
+ DropBlueConnectionsMarker.new(
65
+ blue_hosts: @blue_hosts, switchover_time_ns: @switchover_time_ns, id: @id, active: true
66
+ )
67
+ end
68
+
69
+ # Whether a connection opened to the given host at the given time is now a stale blue connection.
70
+ #
71
+ # @param host [String, nil] the downcased connect-time host of the connection
72
+ # @param open_time_ns [Integer, nil] the monotonic time the connection was opened
73
+ # @return [Boolean]
74
+ def stale?(host, open_time_ns)
75
+ return false unless @active
76
+ return false if host.nil? || open_time_ns.nil?
77
+ return false unless open_time_ns < @switchover_time_ns
78
+
79
+ @blue_hosts.include?(host)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -39,8 +39,10 @@ module AwsAdvancedRubyDriverWrapper
39
39
  build_cluster_pairs(source_status, target_status)
40
40
  end
41
41
 
42
+ # Keyed by downcased host so lookups line up with role_by_host (also downcased); the blue host
43
+ # set the drop-blue-connections marker is built from indexes this map with downcased endpoints.
42
44
  def merge_ips(ip_map)
43
- @host_ip_addresses.merge!(ip_map)
45
+ ip_map.each { |host, ip| @host_ip_addresses[host.downcase] = ip }
44
46
  end
45
47
 
46
48
  def register_role(host_names, role)
@@ -15,6 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  require_relative '../../../logging'
18
+ require_relative '../../../utils/telemetry/telemetry_const'
18
19
 
19
20
  module AwsAdvancedRubyDriverWrapper
20
21
  module Plugins
@@ -24,6 +25,7 @@ module AwsAdvancedRubyDriverWrapper
24
25
  include Logging
25
26
 
26
27
  MINIMUM_SLEEP_CHUNK_SEC = 0.05
28
+ TELEMETRY_SWITCHOVER = 'Blue/Green Deployment switchover'
27
29
 
28
30
  def match?(host_info, role)
29
31
  (@host.nil? || @host.downcase == host_info&.host&.downcase) &&
@@ -39,6 +41,11 @@ module AwsAdvancedRubyDriverWrapper
39
41
  "bgd_id: #{defined?(@bgd_id) ? (@bgd_id || '<null>') : '<n/a>'}]"
40
42
  end
41
43
 
44
+ # Records the time a call is held while the switchover is in progress as a span.
45
+ def in_switchover_span(telemetry_factory, &)
46
+ telemetry_factory.in_context(TELEMETRY_SWITCHOVER, Utils::Telemetry::TraceLevel::NESTED, &)
47
+ end
48
+
42
49
  # Blocks until the Blue/Green switchover phase is no longer IN_PROGRESS, or until the
43
50
  # configured timeout elapses.
44
51
  def wait_while_in_progress(wrapper_props, storage_service, bgd_id, sleep_time_sec = MINIMUM_SLEEP_CHUNK_SEC)
@@ -52,57 +52,37 @@ module AwsAdvancedRubyDriverWrapper
52
52
  bg_plugin: nil)
53
53
  plugin_manager = service_container.plugin_manager
54
54
  dialect_service = service_container.dialect_service
55
+ driver_dialect = dialect_service.driver_dialect
55
56
 
56
57
  unless Utils::RdsUtils.ip?(@substitute_host.host)
57
58
  return open_connection(plugin_manager, @substitute_host, driver_props, wrapper_props, is_initial_connection, is_internal,
58
59
  bg_plugin)
59
60
  end
60
61
 
61
- # mysql2 uses :host for both socket connection and TLS CN verification.
62
- # Connecting to a raw IP with TLS active causes a CN mismatch.
63
- # Fall back to connecting via hostname (DNS will resolve to the correct IP post-switchover),
64
- # matching the effective behaviour of the Java/Go MySQL drivers which use the original
65
- # hostname for TLS regardless of the socket address.
66
- if driver_props[:sslca] && !plugin_manager.plugin_in_use?(Plugins::IamAuthPlugin)
62
+ # The substitute host is a raw IP, so a connection made by hostname could resolve (via DNS)
63
+ # to the old instance during the switchover window. Connect to the IP directly instead.
64
+ if plugin_manager.iam_token_plugin_in_use?
65
+ return apply_iam(service_container, driver_props, wrapper_props, is_initial_connection, bg_plugin)
66
+ end
67
+
68
+ # pg can pin the socket to the IP (hostaddr) while keeping the original hostname for TLS
69
+ # verification, so the pinned connection reaches the right instance and still passes
70
+ # verify-full whatever the CA source.
71
+ pinned_props = driver_dialect.props_pinned_to_ip(driver_props, @substitute_host.host)
72
+ if pinned_props
67
73
  hostname_host = @substitute_host.deep_dup(host: @host)
68
- return open_connection(plugin_manager, hostname_host, driver_props, wrapper_props, is_initial_connection, is_internal,
74
+ return open_connection(plugin_manager, hostname_host, pinned_props, wrapper_props, is_initial_connection, is_internal,
69
75
  bg_plugin)
70
76
  end
71
77
 
72
- if plugin_manager.plugin_in_use?(Plugins::IamAuthPlugin)
73
- if @iam_hosts.nil? || @iam_hosts.empty?
74
- raise StandardError,
75
- 'Connecting with IP address when IAM authentication is enabled requires an \'iam_host\' parameter.'
76
- end
77
-
78
- @iam_hosts.each do |iam_host|
79
- rerouted_host = @substitute_host.deep_dup
80
- rerouted_host.id = iam_host.id
81
- rerouted_host.availability = Host::HostAvailability::AVAILABLE
82
-
83
- rerouted_wrapper_props = wrapper_props.dup
84
- PropertyDefinition::IAM_HOST.set(rerouted_wrapper_props, iam_host.host)
85
- PropertyDefinition::IAM_PORT.set(rerouted_wrapper_props, iam_host.port.to_s) if iam_host.port_specified?
86
-
87
- begin
88
- conn = plugin_manager.internal_connect(
89
- rerouted_host,
90
- driver_props,
91
- rerouted_wrapper_props,
92
- is_initial_connection,
93
- plugin_to_skip: bg_plugin
94
- )
95
- begin
96
- @iam_successful_connect_notify&.call(iam_host.host)
97
- rescue StandardError
98
- nil
99
- end
100
- return conn
101
- rescue StandardError => e
102
- raise unless dialect_service.login_error?(e)
103
- end
104
- end
105
- return nil
78
+ # mysql2/trilogy use :host for both the socket and TLS CN verification, so connecting to a
79
+ # raw IP with TLS active causes a CN mismatch. When TLS is active, fall back to the hostname
80
+ # (DNS resolves to the correct IP post-switchover), matching the effective behaviour of the
81
+ # Java/Go drivers which use the original hostname for TLS regardless of the socket address.
82
+ if driver_props[driver_dialect.tls_ca_property_key]
83
+ hostname_host = @substitute_host.deep_dup(host: @host)
84
+ return open_connection(plugin_manager, hostname_host, driver_props, wrapper_props, is_initial_connection, is_internal,
85
+ bg_plugin)
106
86
  end
107
87
 
108
88
  open_connection(plugin_manager, @substitute_host, driver_props, wrapper_props, is_initial_connection, is_internal, bg_plugin)
@@ -114,6 +94,53 @@ module AwsAdvancedRubyDriverWrapper
114
94
 
115
95
  private
116
96
 
97
+ # Connects to the substitute IP with IAM auth active, trying each configured IAM host in turn
98
+ # so the IAM plugin can resolve the correct token hostname. pg pins the socket to the IP
99
+ # (hostaddr) while presenting the IAM host name for TLS verification; the IAM token is still
100
+ # generated for the IAM host via the IAM_HOST property. Other drivers connect to the IP as-is.
101
+ def apply_iam(service_container, driver_props, wrapper_props, is_initial_connection, bg_plugin)
102
+ plugin_manager = service_container.plugin_manager
103
+ dialect_service = service_container.dialect_service
104
+ driver_dialect = dialect_service.driver_dialect
105
+
106
+ if @iam_hosts.nil? || @iam_hosts.empty?
107
+ raise StandardError,
108
+ 'Connecting with IP address when IAM authentication is enabled requires an \'iam_host\' parameter.'
109
+ end
110
+
111
+ @iam_hosts.each do |iam_host|
112
+ rerouted_host = @substitute_host.deep_dup
113
+ rerouted_host.id = iam_host.id
114
+ rerouted_host.availability = Host::HostAvailability::AVAILABLE
115
+
116
+ rerouted_wrapper_props = wrapper_props.dup
117
+ PropertyDefinition::IAM_HOST.set(rerouted_wrapper_props, iam_host.host)
118
+ PropertyDefinition::IAM_PORT.set(rerouted_wrapper_props, iam_host.port.to_s) if iam_host.port_specified?
119
+
120
+ connect_props = driver_props
121
+ pinned_props = driver_dialect.props_pinned_to_ip(driver_props, @substitute_host.host)
122
+ if pinned_props
123
+ connect_props = pinned_props
124
+ rerouted_host = rerouted_host.deep_dup(host: iam_host.host)
125
+ end
126
+
127
+ begin
128
+ conn = plugin_manager.internal_connect(
129
+ rerouted_host, connect_props, rerouted_wrapper_props, is_initial_connection, plugin_to_skip: bg_plugin
130
+ )
131
+ begin
132
+ @iam_successful_connect_notify&.call(iam_host.host)
133
+ rescue StandardError
134
+ nil
135
+ end
136
+ return conn
137
+ rescue StandardError => e
138
+ raise unless dialect_service.login_error?(e)
139
+ end
140
+ end
141
+ nil
142
+ end
143
+
117
144
  # Opens a connection through the plugin pipeline. Monitoring connections (is_internal: true)
118
145
  # must use internal_connect so they do NOT mutate the shared current connection — otherwise
119
146
  # one monitor thread's connect would close another monitor thread's live connection via
@@ -35,8 +35,10 @@ module AwsAdvancedRubyDriverWrapper
35
35
  storage_service = service_container.storage_service
36
36
  hold_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
37
 
38
- wait_while_in_progress(wrapper_props, storage_service, @bgd_id, SLEEP_TIME_SEC) do |timeout_sec|
39
- "Blue/Green Deployment switchover is still in progress after #{timeout_sec} s. Try to connect again later."
38
+ in_switchover_span(service_container.telemetry_factory) do
39
+ wait_while_in_progress(wrapper_props, storage_service, @bgd_id, SLEEP_TIME_SEC) do |timeout_sec|
40
+ "Blue/Green Deployment switchover is still in progress after #{timeout_sec} s. Try to connect again later."
41
+ end
40
42
  end
41
43
 
42
44
  elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - hold_start_time) * 1000).round
@@ -31,11 +31,13 @@ module AwsAdvancedRubyDriverWrapper
31
31
  end
32
32
 
33
33
  # Blocks until switchover is no longer IN_PROGRESS
34
- def apply(method_name, wrapper_props, storage_service)
34
+ def apply(method_name, wrapper_props, storage_service, telemetry_factory:)
35
35
  hold_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
36
 
37
- wait_while_in_progress(wrapper_props, storage_service, @bgd_id, SLEEP_TIME_SEC) do |timeout_sec|
38
- "Blue/Green Deployment switchover is still in progress after #{timeout_sec} s. Try '#{method_name}' again later."
37
+ in_switchover_span(telemetry_factory) do
38
+ wait_while_in_progress(wrapper_props, storage_service, @bgd_id, SLEEP_TIME_SEC) do |timeout_sec|
39
+ "Blue/Green Deployment switchover is still in progress after #{timeout_sec} s. Try '#{method_name}' again later."
40
+ end
39
41
  end
40
42
 
41
43
  elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - hold_start_time) * 1000).round
@@ -35,8 +35,14 @@ module AwsAdvancedRubyDriverWrapper
35
35
  # Blocks until a corresponding green host is found for host_info.host, the switchover
36
36
  # completes, or the timeout elapses.
37
37
  def apply(host_info, _, wrapper_props, _is_initial_connection, service_container, is_internal: false, bg_plugin: nil)
38
- storage_service = service_container.storage_service
38
+ in_switchover_span(service_container.telemetry_factory) do
39
+ hold_until_corresponding_host_found(host_info, wrapper_props, service_container.storage_service)
40
+ end
41
+ end
42
+
43
+ private
39
44
 
45
+ def hold_until_corresponding_host_found(host_info, wrapper_props, storage_service)
40
46
  bg_status = storage_service.get(BlueGreenPlugin::BLUE_GREEN_NAME, @bgd_id)
41
47
  corresponding_pair = bg_status&.corresponding_hosts&.[](host_info.host)
42
48
 
@@ -223,7 +223,7 @@ module AwsAdvancedRubyDriverWrapper
223
223
  next [] if host_pair.nil?
224
224
 
225
225
  blue_host_spec = host_pair[0]
226
- blue_ip_entry = @host_mapper.host_ip_addresses[blue_host_spec.host]
226
+ blue_ip_entry = @host_mapper.host_ip_addresses[blue_host_spec.host.downcase]
227
227
  blue_ip_host_spec = blue_ip_entry ? Host::HostInfo.new(host: blue_ip_entry, port: blue_host_spec.port,
228
228
  role: blue_host_spec.role) : blue_host_spec
229
229
 
@@ -394,7 +394,7 @@ module AwsAdvancedRubyDriverWrapper
394
394
  end
395
395
 
396
396
  def iam_enabled?
397
- @service_container.plugin_manager.plugin_in_use?(Plugins::IamAuthPlugin)
397
+ @service_container.plugin_manager.iam_token_plugin_in_use?
398
398
  end
399
399
 
400
400
  def driver_dialect
@@ -421,14 +421,6 @@ module AwsAdvancedRubyDriverWrapper
421
421
  true
422
422
  end
423
423
 
424
- def syntax_error?(err)
425
- driver_dialect.sql_state(err)&.start_with?('42') || false
426
- end
427
-
428
- def db_error?(err)
429
- !driver_dialect.sql_state(err).nil?
430
- end
431
-
432
424
  # @note Callers must hold @connection_mutex (invoke within a @connection_mutex.synchronize block).
433
425
  def status_available?(conn)
434
426
  dialect = @service_container.dialect_service.db_dialect
@@ -508,11 +500,11 @@ module AwsAdvancedRubyDriverWrapper
508
500
  end
509
501
 
510
502
  def handle_collect_status_error(err, conn)
511
- if syntax_error?(err)
503
+ if @service_container.dialect_service.schema_error?(err)
512
504
  @current_phase = Phase::NOT_CREATED
513
505
  logger.warn { "[#{@role}] current_phase: #{@current_phase}, exception while querying for blue/green status." }
514
- elsif db_error?(err)
515
- if err.message.include?('An error occurred while retrieving the blue/green fast switchover metadata')
506
+ elsif @service_container.dialect_service.database_error?(err)
507
+ if err.message.include?('while retrieving the blue/green fast switchover metadata')
516
508
  @current_phase = Phase::NOT_CREATED
517
509
  return
518
510
  end
@@ -557,7 +549,8 @@ module AwsAdvancedRubyDriverWrapper
557
549
  @service_container.plugin_manager,
558
550
  @service_container.session_state_service,
559
551
  @service_container.storage_service,
560
- @service_container.monitor_service
552
+ @service_container.monitor_service,
553
+ @service_container.telemetry_factory
561
554
  )
562
555
 
563
556
  @host_list_provider = @service_container.dialect_service.db_dialect.create_host_list_provider(scoped_container)