aws_advanced_ruby_driver_wrapper 1.0.0 → 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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -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_action_cable_postgresql_support.rb +40 -0
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_connection_handler.rb +47 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +23 -9
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +65 -10
  10. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +61 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +48 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -81
  16. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +28 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  20. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +11 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +14 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +6 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +125 -7
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +10 -13
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +95 -3
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +23 -1
  53. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +49 -23
  54. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  55. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  57. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  58. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +23 -4
  59. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  60. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  62. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +18 -7
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +29 -8
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/utils/{ar_constants.rb → telemetry/open_telemetry_counter.rb} +19 -5
  80. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  85. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  86. data/lib/aws_advanced_ruby_driver_wrapper.rb +47 -2
  87. metadata +57 -3
@@ -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
 
@@ -338,6 +338,10 @@ module AwsAdvancedRubyDriverWrapper
338
338
  @event.reset
339
339
  end
340
340
 
341
+ def abandon_connections
342
+ @connection.abandon
343
+ end
344
+
341
345
  private
342
346
 
343
347
  def attempt_open_connection
@@ -390,7 +394,7 @@ module AwsAdvancedRubyDriverWrapper
390
394
  end
391
395
 
392
396
  def iam_enabled?
393
- @service_container.plugin_manager.plugin_in_use?(Plugins::IamAuthPlugin)
397
+ @service_container.plugin_manager.iam_token_plugin_in_use?
394
398
  end
395
399
 
396
400
  def driver_dialect
@@ -417,14 +421,6 @@ module AwsAdvancedRubyDriverWrapper
417
421
  true
418
422
  end
419
423
 
420
- def syntax_error?(err)
421
- driver_dialect.sql_state(err)&.start_with?('42') || false
422
- end
423
-
424
- def db_error?(err)
425
- !driver_dialect.sql_state(err).nil?
426
- end
427
-
428
424
  # @note Callers must hold @connection_mutex (invoke within a @connection_mutex.synchronize block).
429
425
  def status_available?(conn)
430
426
  dialect = @service_container.dialect_service.db_dialect
@@ -504,11 +500,11 @@ module AwsAdvancedRubyDriverWrapper
504
500
  end
505
501
 
506
502
  def handle_collect_status_error(err, conn)
507
- if syntax_error?(err)
503
+ if @service_container.dialect_service.schema_error?(err)
508
504
  @current_phase = Phase::NOT_CREATED
509
505
  logger.warn { "[#{@role}] current_phase: #{@current_phase}, exception while querying for blue/green status." }
510
- elsif db_error?(err)
511
- if err.message.include?('An error occurred while retrieving the blue/green fast switchover metadata')
506
+ elsif @service_container.dialect_service.database_error?(err)
507
+ if err.message.include?('while retrieving the blue/green fast switchover metadata')
512
508
  @current_phase = Phase::NOT_CREATED
513
509
  return
514
510
  end
@@ -553,7 +549,8 @@ module AwsAdvancedRubyDriverWrapper
553
549
  @service_container.plugin_manager,
554
550
  @service_container.session_state_service,
555
551
  @service_container.storage_service,
556
- @service_container.monitor_service
552
+ @service_container.monitor_service,
553
+ @service_container.telemetry_factory
557
554
  )
558
555
 
559
556
  @host_list_provider = @service_container.dialect_service.db_dialect.create_host_list_provider(scoped_container)
@@ -18,6 +18,7 @@ require 'concurrent'
18
18
  require_relative '../../logging'
19
19
  require_relative '../../property_definition'
20
20
  require_relative '../../utils/rds_utils'
21
+ require_relative 'drop_blue_connections_marker'
21
22
  require_relative 'iam_host_tracker'
22
23
  require_relative 'interval_rate'
23
24
  require_relative 'host_mapper'
@@ -61,6 +62,10 @@ module AwsAdvancedRubyDriverWrapper
61
62
  @process_status_lock = Mutex.new
62
63
  @monitor_generation = 0
63
64
 
65
+ @drop_blue_connections = PropertyDefinition::BG_DROP_BLUE_CONNECTIONS.get_bool(wrapper_props)
66
+ # Identity of the drop-blue-connections marker created for the current switchover, if any.
67
+ @drop_marker_id = nil
68
+
64
69
  @status_check_interval_map = {
65
70
  IntervalRate::BASELINE => PropertyDefinition::BG_INTERVAL_BASELINE_SEC.get_float(wrapper_props) * 1000,
66
71
  IntervalRate::INCREASED => PropertyDefinition::BG_INTERVAL_INCREASED_SEC.get_float(wrapper_props) * 1000,
@@ -93,6 +98,11 @@ module AwsAdvancedRubyDriverWrapper
93
98
  @monitors.each_value { |m| m&.stop }
94
99
  end
95
100
 
101
+ # Detaches the monitors inherited by a forked child without closing the parent's connections.
102
+ def release_after_fork
103
+ @monitors.each_value { |m| m&.release_after_fork }
104
+ end
105
+
96
106
  def log_switchover_final_summary
97
107
  return unless switchover_finalized?
98
108
 
@@ -171,6 +181,18 @@ module AwsAdvancedRubyDriverWrapper
171
181
 
172
182
  logger.warn { 'Blue/Green switchover has timed out.' }
173
183
  @summary_status = @rollback ? @builder.created : @builder.completed(@rollback)
184
+ # A timeout means the wrapper stopped waiting, not that the switchover is confirmed. Only
185
+ # activate the marker if the switchover was last observed at POST (past the point of no
186
+ # return); timing out in PREPARATION/IN_PROGRESS could still roll back, and dropping blue
187
+ # connections then would hit a blue that is still primary. In that case discard the pending
188
+ # marker so it does not linger unactivated for the full marker TTL: we have given up waiting
189
+ # on this switchover, and no later phase will arrive to activate it.
190
+ if @latest_status_phase == Phase::POST
191
+ activate_drop_marker
192
+ else
193
+ remove_drop_marker
194
+ @drop_marker_id = nil
195
+ end
174
196
  update_monitors
175
197
  update_status_cache
176
198
  log_current_context
@@ -186,7 +208,15 @@ module AwsAdvancedRubyDriverWrapper
186
208
  @monitors = { Role::SOURCE => nil, Role::TARGET => nil }
187
209
  old_monitors.each_value { |m| m&.stop }
188
210
 
189
- Plugins::IamAuthPlugin.clear_cache(@storage_service) if iam_enabled?
211
+ # A completed switchover's marker must survive this reset so connections opened to the old
212
+ # blue environment are still invalidated on their next use. A rollback leaves blue as primary,
213
+ # so its marker is discarded instead. Either way a later switchover starts a fresh marker.
214
+ remove_drop_marker if @rollback
215
+ @drop_marker_id = nil
216
+
217
+ # IamAuthPlugin, FederatedAuthPlugin, and OktaAuthPlugin all cache tokens under the same
218
+ # shared IAM_TOKEN_CACHE_NAME, so clearing through any one of them clears all of them.
219
+ Plugins::IamAuthPlugin.clear_cache(@storage_service) if iam_token_cache_in_use?
190
220
 
191
221
  @rollback = false
192
222
  @green_topology_recognized_logged = false
@@ -220,12 +250,15 @@ module AwsAdvancedRubyDriverWrapper
220
250
  @switchover_state.update_dns_flags(@bgd_id, role, interim_status)
221
251
  @summary_status = @builder.in_progress(@rollback)
222
252
  @switchover_state.trigger_in_progress_monitor_reset
253
+ ensure_drop_marker_pending
223
254
  when Phase::POST
224
255
  @switchover_state.update_dns_flags(@bgd_id, role, interim_status)
225
256
  @summary_status = @builder.post(@rollback)
257
+ activate_drop_marker
226
258
  when Phase::COMPLETED
227
259
  @switchover_state.update_dns_flags(@bgd_id, role, interim_status)
228
260
  @summary_status = @builder.completed(@rollback)
261
+ activate_drop_marker
229
262
  else
230
263
  raise ArgumentError, "[bgd_id: '#{@bgd_id}'] Unknown BG phase '#{@latest_status_phase}'."
231
264
  end
@@ -345,14 +378,73 @@ module AwsAdvancedRubyDriverWrapper
345
378
  )
346
379
  end
347
380
 
381
+ # Creates a pending drop-blue-connections marker the first time a switchover reaches IN_PROGRESS.
382
+ # The marker is only confirmed (see #activate_drop_marker) once the switchover passes the point
383
+ # of no return, so nothing is invalidated while it is still in progress and could roll back.
384
+ def ensure_drop_marker_pending
385
+ return unless @drop_blue_connections
386
+ return if @rollback
387
+ return if @drop_marker_id
388
+
389
+ hosts = blue_host_set
390
+ return if hosts.empty?
391
+
392
+ marker = DropBlueConnectionsMarker.new(blue_hosts: hosts, switchover_time_ns: nano_time)
393
+ @drop_marker_id = marker.id
394
+ store_drop_marker(marker)
395
+ end
396
+
397
+ # Confirms the switchover's pending marker so stale blue connections begin to be invalidated.
398
+ # A marker is only created when IN_PROGRESS is observed (see #ensure_drop_marker_pending), since
399
+ # that is the only point with a reliable switchover time. A provider that first sees the
400
+ # deployment at POST/COMPLETED cannot tell which of its connections predate the switchover and
401
+ # would wrongly cover connections opened after switchover, including to green.
402
+ def activate_drop_marker
403
+ return unless @drop_blue_connections
404
+ return if @rollback
405
+
406
+ marker = read_drop_marker
407
+ return if marker.nil? || marker.active?
408
+
409
+ store_drop_marker(marker.activated)
410
+ end
411
+
412
+ # The blue host set: downcased SOURCE endpoint names plus their pre-switchover IP addresses.
413
+ def blue_host_set
414
+ return Set.new unless @summary_status
415
+
416
+ blue = @summary_status.role_by_host.select { |_host, role| role == Role::SOURCE }.keys
417
+ ips = blue.filter_map { |host| @host_mapper.host_ip_addresses[host] }
418
+ (blue + ips).to_set(&:downcase)
419
+ end
420
+
421
+ def store_drop_marker(marker)
422
+ @storage_service.set(BlueGreenPlugin::DROP_MARKER_NAME, BlueGreenPlugin.drop_marker_key(@bgd_id), marker)
423
+ end
424
+
425
+ def read_drop_marker
426
+ @storage_service.get(BlueGreenPlugin::DROP_MARKER_NAME, BlueGreenPlugin.drop_marker_key(@bgd_id), register_access: false)
427
+ end
428
+
429
+ def remove_drop_marker
430
+ @storage_service.remove(BlueGreenPlugin::DROP_MARKER_NAME, BlueGreenPlugin.drop_marker_key(@bgd_id))
431
+ end
432
+
433
+ def nano_time
434
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
435
+ end
436
+
348
437
  def switchover_finalized?
349
438
  completed = (!@rollback && @summary_status.current_phase == Phase::COMPLETED) ||
350
439
  (@rollback && @summary_status.current_phase == Phase::CREATED)
351
440
  completed && @event_log.any? { |_, v| v.phase&.active_switchover_or_completed? }
352
441
  end
353
442
 
354
- def iam_enabled?
355
- @service_container.plugin_manager&.plugin_in_use?(Plugins::IamAuthPlugin) || false
443
+ # True when any auth plugin backed by the shared IAM token cache (IAM or AD FS
444
+ # or Okta) is in use, so a switchover knows to purge that cache regardless
445
+ # of which one is active.
446
+ def iam_token_cache_in_use?
447
+ @service_container.plugin_manager&.iam_token_plugin_in_use? || false
356
448
  end
357
449
 
358
450
  def context_hash
@@ -41,7 +41,14 @@ module AwsAdvancedRubyDriverWrapper
41
41
  refresh_rate_sec,
42
42
  refresh_rate_backoff_factor,
43
43
  max_refresh_rate_sec,
44
- rds_client_func: ->(_, region) { Aws::RDS::Client.new(region: region) }
44
+ credentials_provider: nil,
45
+ rds_client_func: lambda { |_, client_region|
46
+ # Pass the configured credentials provider when one is set; otherwise omit :credentials
47
+ # so the SDK resolves from its default chain.
48
+ options = { region: client_region }
49
+ options[:credentials] = credentials_provider if credentials_provider
50
+ Aws::RDS::Client.new(**options)
51
+ }
45
52
  )
46
53
  super(termination_timeout_sec: TERMINATION_TIMEOUT_SEC)
47
54
 
@@ -58,6 +65,7 @@ module AwsAdvancedRubyDriverWrapper
58
65
  @refresh_cv = ConditionVariable.new
59
66
  @refresh_required = false
60
67
  @connection_issue = false
68
+ @info_changed_counter = service_container.telemetry_factory.create_counter('custom_endpoint.info_changed.count')
61
69
  end
62
70
 
63
71
  def endpoint_info?
@@ -161,6 +169,7 @@ module AwsAdvancedRubyDriverWrapper
161
169
  logger.debug("[#{@endpoint_id}] Custom endpoint info changed: #{endpoint_info}")
162
170
  cache_allowed_blocked(endpoint_info)
163
171
  cache_info(endpoint_info)
172
+ @info_changed_counter.inc
164
173
  speedup_refresh_rate
165
174
 
166
175
  elapsed = monotonic_time - start
@@ -241,6 +250,7 @@ module AwsAdvancedRubyDriverWrapper
241
250
  end
242
251
 
243
252
  def cached_info
253
+ # Explicitly unregistered: a monitor reading its own data must not keep itself alive.
244
254
  storage_service.get(ENDPOINT_INFO_CACHE_NAME, @custom_endpoint_host.url, register_access: false)
245
255
  end
246
256
 
@@ -38,6 +38,7 @@ module AwsAdvancedRubyDriverWrapper
38
38
  @props = props
39
39
  @should_wait_for_info = PropertyDefinition::WAIT_FOR_CUSTOM_ENDPOINT_INFO.get_bool(props)
40
40
  @wait_timeout_sec = PropertyDefinition::WAIT_FOR_CUSTOM_ENDPOINT_INFO_TIMEOUT_SEC.get_float(props)
41
+ @wait_for_info_counter = service_container.telemetry_factory.create_counter('custom_endpoint.wait_for_info.count')
41
42
 
42
43
  @custom_endpoint_host = nil
43
44
  @endpoint_id = nil
@@ -128,7 +129,8 @@ module AwsAdvancedRubyDriverWrapper
128
129
  @region,
129
130
  PropertyDefinition::CUSTOM_ENDPOINT_INFO_REFRESH_RATE_SEC.get_float(props),
130
131
  PropertyDefinition::CUSTOM_ENDPOINT_INFO_REFRESH_RATE_BACKOFF_FACTOR.get_int(props),
131
- PropertyDefinition::CUSTOM_ENDPOINT_INFO_MAX_REFRESH_RATE_SEC.get_float(props)
132
+ PropertyDefinition::CUSTOM_ENDPOINT_INFO_MAX_REFRESH_RATE_SEC.get_float(props),
133
+ credentials_provider: PropertyDefinition::AWS_CREDENTIALS_PROVIDER.get(props)
132
134
  )
133
135
  end
134
136
  end
@@ -137,6 +139,7 @@ module AwsAdvancedRubyDriverWrapper
137
139
  return if monitor.endpoint_info?
138
140
 
139
141
  monitor.request_endpoint_info_update
142
+ @wait_for_info_counter.inc
140
143
  logger.debug("CustomEndpointPlugin: waiting up to #{@wait_timeout_sec}s for endpoint info on #{@custom_endpoint_host.url}")
141
144
 
142
145
  return if monitor.wait_for_info?(@wait_timeout_sec)
@@ -18,6 +18,7 @@ require 'concurrent'
18
18
  require_relative '../errors'
19
19
  require_relative '../host/host_info'
20
20
  require_relative '../host/host_availability'
21
+ require_relative '../utils/telemetry/telemetry_const'
21
22
 
22
23
  module AwsAdvancedRubyDriverWrapper
23
24
  module Plugins
@@ -44,7 +45,7 @@ module AwsAdvancedRubyDriverWrapper
44
45
  host_info
45
46
  end
46
47
 
47
- conn = driver_dialect.connect(target_host_info, driver_props)
48
+ conn = with_driver_span { driver_dialect.connect(target_host_info, driver_props) }
48
49
 
49
50
  if conn.nil?
50
51
  raise Errors::AwsError,
@@ -83,7 +84,7 @@ module AwsAdvancedRubyDriverWrapper
83
84
 
84
85
  def internal_connect(host_info, driver_props, _wrapper_props, _is_initial_connection, _pipeline_callable)
85
86
  driver_dialect = @service_container.dialect_service.driver_dialect
86
- driver_dialect.connect(host_info, driver_props)
87
+ with_driver_span { driver_dialect.connect(host_info, driver_props) }
87
88
  end
88
89
 
89
90
  def execute(target_method_name, target_callable, *args, **, &)
@@ -92,7 +93,7 @@ module AwsAdvancedRubyDriverWrapper
92
93
  succeeded = false
93
94
 
94
95
  begin
95
- result = target_callable.call(*args, **, &)
96
+ result = with_driver_span { target_callable.call(*args, **, &) }
96
97
  succeeded = true
97
98
  result
98
99
  ensure
@@ -103,6 +104,16 @@ module AwsAdvancedRubyDriverWrapper
103
104
  end
104
105
  end
105
106
  end
107
+
108
+ private
109
+
110
+ # Records the time spent in the driver itself as a span named after the driver.
111
+ def with_driver_span(&)
112
+ telemetry_factory = @service_container.telemetry_factory
113
+ return yield unless telemetry_factory.traces_in_use?
114
+
115
+ telemetry_factory.in_context(@service_container.dialect_service.driver_dialect.name, Utils::Telemetry::TraceLevel::NESTED, &)
116
+ end
106
117
  end
107
118
  end
108
119
  end