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,167 @@
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 '../aws_advanced_ruby_driver_wrapper'
18
+ require_relative 'utils/connection_config_parser'
19
+ require_relative 'utils/sql_encoding'
20
+ require_relative 'services/service_utility'
21
+ require_relative 'ruby_method'
22
+ require_relative 'errors'
23
+
24
+ module AwsAdvancedRubyDriverWrapper
25
+ # A drop-in for a Trilogy connection. Trilogy has no prepared statements and no asynchronous or
26
+ # streaming reads, and it reads a result off the wire in full before returning it, so the only calls
27
+ # that reach the server are made on the connection itself, and results are handed back unwrapped.
28
+ class WrapperTrilogyClient
29
+ # Trilogy.new takes its options as a single hash, so this does too, and keyword arguments as well.
30
+ def self.new(options = {}, **)
31
+ instance = allocate
32
+ instance.send(:initialize, options, **)
33
+ instance
34
+ end
35
+
36
+ def initialize(options = {}, **)
37
+ ensure_trilogy!
38
+ config = Utils::ConnectionConfigParser.parse(:trilogy, **options, **)
39
+ @service_container = Services::ServiceUtility.create_standard_container(config)
40
+ @service_container.host_service.refresh_host_list
41
+ @query_conn = nil
42
+ @last_sql = nil
43
+ conn_service = @service_container.connection_service
44
+ @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
45
+ end
46
+
47
+ # Explicitly define critical methods (bypass method_missing to avoid method_missing overhead)
48
+
49
+ # The connection a statement was sent on is remembered, and so is its SQL, because the further
50
+ # result sets a statement leaves are read by calls of their own (next_result, abandon_results!),
51
+ # which carry neither and can only be made on that connection.
52
+ #
53
+ # What is remembered is the copy of the SQL that plugins inspect. The driver is sent the SQL as
54
+ # the application wrote it.
55
+ def query(sql)
56
+ ensure_connected!
57
+ inspected = Utils::SqlEncoding.inspectable(sql)
58
+ result = pm.execute(RubyMethod::CONNECTION_QUERY, current_conn, ->(*a) { current_conn.query(*a) }, sql,
59
+ sql: inspected)
60
+ @query_conn = current_conn
61
+ @last_sql = inspected
62
+ result
63
+ end
64
+
65
+ def ping
66
+ ensure_connected!
67
+ pm.execute(RubyMethod::CONNECTION_PING, current_conn, -> { current_conn.ping })
68
+ end
69
+
70
+ def close
71
+ return if current_conn.nil?
72
+
73
+ pm.execute(RubyMethod::CONNECTION_CLOSE, current_conn, -> { current_conn.close })
74
+ end
75
+
76
+ def next_result
77
+ pm.execute(RubyMethod::CONNECTION_NEXT_RESULT, current_conn, -> { current_conn.next_result },
78
+ bounded_conn: @query_conn, sql: @last_sql)
79
+ end
80
+
81
+ # Trilogy's own query_with_flags calls query on the driver connection directly, so it is defined
82
+ # here to send the statement through the plugin pipeline instead.
83
+ def query_with_flags(sql, flags)
84
+ ensure_connected!
85
+ old_flags = current_conn.query_flags
86
+ current_conn.query_flags = flags
87
+ query(sql)
88
+ ensure
89
+ current_conn.query_flags = old_flags if old_flags
90
+ end
91
+
92
+ # -- method_missing: covers non-network calls and rarely used network calls --
93
+
94
+ # The network calls that are rare enough not to be worth a method of their own. They are entered
95
+ # into the pipeline under the name the pipeline knows them by rather than as a bare string, so
96
+ # that the connection each is bound to is checked.
97
+ DYNAMIC_METHODS = {
98
+ abandon_results!: RubyMethod::CONNECTION_ABANDON_RESULTS,
99
+ change_db: RubyMethod::CONNECTION_CHANGE_DB,
100
+ set_server_option: RubyMethod::CONNECTION_SET_SERVER_OPTION
101
+ }.freeze
102
+
103
+ # Draining what is left of a statement can only be done on the connection it was sent on.
104
+ BOUNDED_TO_QUERY = Set[:abandon_results!].freeze
105
+
106
+ def method_missing(method_name, *, **, &)
107
+ conn = ensure_connected!
108
+ raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
109
+
110
+ method_key = "connection.#{method_name}"
111
+ return conn.send(method_name, *, **, &) unless network_bound_methods.include?(method_key)
112
+
113
+ bounded_conn = BOUNDED_TO_QUERY.include?(method_name) ? @query_conn : nil
114
+ pm.execute(
115
+ DYNAMIC_METHODS[method_name] || method_key, current_conn,
116
+ ->(*a, **opts, &b) { current_conn.send(method_name, *a, **opts, &b) },
117
+ *, **, bounded_conn: bounded_conn, &
118
+ )
119
+ end
120
+
121
+ def respond_to_missing?(method, include_private = false)
122
+ current_conn.respond_to?(method, include_private) || super
123
+ end
124
+
125
+ # A concise representation that never exposes the connection config (which carries
126
+ # credentials) or the cached SQL text this instance holds. Defined so a default
127
+ # dump - via logging, interpolation, `pp`, or a backtrace - cannot leak either.
128
+ def inspect
129
+ format('#<%<class>s:0x%<addr>016x>', class: self.class.name, addr: object_id << 1)
130
+ end
131
+ alias to_s inspect
132
+
133
+ def pretty_print(pp)
134
+ pp.text(inspect)
135
+ end
136
+
137
+ private
138
+
139
+ def ensure_trilogy!
140
+ require 'trilogy'
141
+ rescue LoadError
142
+ raise LoadError, "WrapperTrilogyClient requires 'trilogy'. Add it to your Gemfile: gem 'trilogy'"
143
+ end
144
+
145
+ def current_conn
146
+ @service_container.connection_service.current_connection
147
+ end
148
+
149
+ # Guards the entry points that reach for the current connection directly (the query paths and
150
+ # method_missing). Without a live connection AR expects the driver's own "not connected" error,
151
+ # which it translates to a connection error; a bare NoMethodError on nil would not be recognized.
152
+ def ensure_connected!
153
+ conn = current_conn
154
+ raise Trilogy::BaseConnectionError, 'MySQL client is not connected' if conn.nil?
155
+
156
+ conn
157
+ end
158
+
159
+ def pm
160
+ @service_container.plugin_manager
161
+ end
162
+
163
+ def network_bound_methods
164
+ @network_bound_methods ||= @service_container.dialect_service.driver_dialect.network_bound_methods
165
+ end
166
+ end
167
+ end
@@ -49,7 +49,7 @@ module AwsAdvancedRubyDriverWrapper
49
49
  end
50
50
 
51
51
  def cluster_id
52
- PropertyDefinition::CLUSTER_ID.get(wrapper_props)
52
+ PropertyDefinition::CLUSTER_ID.get_string(wrapper_props)
53
53
  end
54
54
 
55
55
  # Redact sensitive property values (passwords, IAM tokens, Secrets Manager
@@ -32,9 +32,10 @@ module AwsAdvancedRubyDriverWrapper
32
32
  module_function
33
33
 
34
34
  # Main entry point. The driver_name is provided by the wrapper class
35
- # (e.g. :postgresql from WrapperPgConnection, :mysql2 from WrapperMysql2Client).
35
+ # (e.g. :postgresql from WrapperPgConnection, :mysql2 from WrapperMysql2Client, :trilogy from
36
+ # WrapperTrilogyClient).
36
37
  #
37
- # @param driver_name [Symbol] :postgresql or :mysql2
38
+ # @param driver_name [Symbol] :postgresql, :mysql2 or :trilogy
38
39
  def parse(driver_name, *args, **kwargs)
39
40
  if driver_name == :mysql2 && !args.empty? && !args.first.is_a?(Hash)
40
41
  raise ArgumentError,
@@ -192,7 +193,9 @@ module AwsAdvancedRubyDriverWrapper
192
193
  # Validates that values in a config hash match the expected type of their corresponding WrapperProperty.
193
194
  # Raises TypeError if a value is present but incompatible. Strings that represent valid integers
194
195
  # are accepted for Integer-typed properties. Float-typed properties also accept Integers and
195
- # strings that represent a valid integer or decimal number.
196
+ # strings that represent a valid integer or decimal number. String-typed properties also accept
197
+ # numbers and Symbol-typed properties also accept strings, since YAML (e.g. database.yml) parses
198
+ # unquoted values like `cluster_id: 1` as numbers and never produces symbols.
196
199
  def validate_props!(config)
197
200
  config.each do |key, value|
198
201
  prop = PropertyDefinition::KNOWN_PROPERTIES[key]
@@ -200,23 +203,30 @@ module AwsAdvancedRubyDriverWrapper
200
203
  next if value.nil?
201
204
  next if prop.type.nil?
202
205
 
203
- valid = if prop.type == :boolean
204
- value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
205
- (value.is_a?(String) && BOOLEAN_STRINGS.include?(value.downcase))
206
- elsif prop.type == Float
207
- value.is_a?(Float) || value.is_a?(Integer) ||
208
- (value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/))
209
- else
210
- value.is_a?(prop.type) ||
211
- (prop.type == Integer && value.is_a?(String) && value.match?(/\A-?\d+\z/))
212
- end
213
-
214
- raise TypeError, "#{key}: expected #{prop.type}, got #{value.class}" unless valid
206
+ raise TypeError, "#{key}: expected #{prop.type}, got #{value.class}" unless valid_type?(prop.type, value)
215
207
 
216
208
  prop.validate!(value)
217
209
  end
218
210
  end
219
211
 
212
+ def valid_type?(type, value)
213
+ if type == :boolean
214
+ value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
215
+ (value.is_a?(String) && BOOLEAN_STRINGS.include?(value.downcase))
216
+ elsif type == Float
217
+ value.is_a?(Float) || value.is_a?(Integer) ||
218
+ (value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/))
219
+ elsif type == Integer
220
+ value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A-?\d+\z/))
221
+ elsif type == String
222
+ value.is_a?(String) || value.is_a?(Numeric)
223
+ elsif type == Symbol
224
+ value.is_a?(Symbol) || value.is_a?(String)
225
+ else
226
+ value.is_a?(type)
227
+ end
228
+ end
229
+
220
230
  # Forms a HostInfo object from a URI host section string.
221
231
  # Each host entry may or may not have a port; missing ports are represented as -1.
222
232
  # The resulting port is a comma-delimited string of per-host ports.
@@ -14,6 +14,7 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'concurrent'
17
18
  require_relative '../../logging'
18
19
 
19
20
  module AwsAdvancedRubyDriverWrapper
@@ -23,10 +24,11 @@ module AwsAdvancedRubyDriverWrapper
23
24
  # Batches deduplicate events via Set semantics (eql?/hash).
24
25
  #
25
26
  # Public API:
26
- # subscribe(subscriber, event_classes) — register for event types
27
- # unsubscribe(subscriber, event_classes) — deregister
28
- # publish(event) — deliver immediate or queue batched
29
- # release_resources — stop background thread
27
+ # subscribe(subscriber, event_classes) - register for event types
28
+ # unsubscribe(subscriber, event_classes) - deregister
29
+ # publish(event) - deliver immediate or queue batched
30
+ # release_resources - stop background thread
31
+ # restart_after_fork - restart background thread in a forked child
30
32
  class BatchingEventPublisher
31
33
  include Logging
32
34
 
@@ -37,7 +39,7 @@ module AwsAdvancedRubyDriverWrapper
37
39
  @subscribers = {}
38
40
  @event_queue = Set.new
39
41
  @lock = Mutex.new
40
- @running = true
42
+ @running = Concurrent::AtomicBoolean.new(true)
41
43
  @thread = start_publishing_thread
42
44
  end
43
45
 
@@ -70,7 +72,7 @@ module AwsAdvancedRubyDriverWrapper
70
72
  end
71
73
 
72
74
  def release_resources
73
- @lock.synchronize { @running = false }
75
+ @running.make_false
74
76
  begin
75
77
  @thread&.wakeup
76
78
  rescue ThreadError
@@ -79,11 +81,20 @@ module AwsAdvancedRubyDriverWrapper
79
81
  @thread&.join(@message_interval_sec)
80
82
  end
81
83
 
84
+ # Restarts the publishing thread in a forked child, where it no longer runs. A thread that is
85
+ # still alive is kept, so calling this again does not start a second one.
86
+ def restart_after_fork
87
+ return if @thread&.alive?
88
+
89
+ @running.make_true
90
+ @thread = start_publishing_thread
91
+ end
92
+
82
93
  private
83
94
 
84
95
  def start_publishing_thread
85
96
  thread = Thread.new do
86
- while @lock.synchronize { @running }
97
+ while @running.true?
87
98
  sleep(@message_interval_sec)
88
99
  send_messages
89
100
  end
@@ -35,9 +35,9 @@ module AwsAdvancedRubyDriverWrapper
35
35
  def query_global_topology(conn, initial_host_info, instance_templates_by_region)
36
36
  results = @dialect.execute(conn, @dialect.topology_query)
37
37
 
38
- if results.fields.empty?
38
+ if topology_result_empty?(results)
39
39
  # We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred.
40
- logger.debug('The topology query returned a result with 0 columns. ' \
40
+ logger.debug('The topology query returned a result with no rows or rows with no columns. ' \
41
41
  'This may occur if the topology query is executed when the server is failing over.')
42
42
  return nil
43
43
  end
@@ -19,10 +19,14 @@ require_relative 'rds_utils'
19
19
  require_relative 'rds_url_type'
20
20
  require_relative '../property_definition'
21
21
  require_relative '../logging'
22
+ require_relative 'telemetry/null_telemetry'
23
+ require_relative 'telemetry/telemetry_const'
22
24
 
23
25
  module AwsAdvancedRubyDriverWrapper
24
26
  module Utils
25
27
  module IamAuthUtils
28
+ TELEMETRY_FETCH_TOKEN = 'fetch authentication token'
29
+
26
30
  module_function
27
31
 
28
32
  TokenEntry = Data.define(:token, :expires_at) do
@@ -83,10 +87,15 @@ module AwsAdvancedRubyDriverWrapper
83
87
  arn&.match(/\Aarn:aws(?:-[a-z]+)*:rds:(?<region>[^:]+)/)&.[](:region)
84
88
  end
85
89
 
86
- def generate_token(region:, hostname:, port:, user:, credentials_provider:)
87
- Aws::RDS::AuthTokenGenerator
88
- .new(credentials: credentials_provider)
89
- .auth_token(region:, endpoint: "#{hostname}:#{port}", user_name: user)
90
+ # @param telemetry_factory [Telemetry::TelemetryFactory, Telemetry::NullTelemetryFactory] records
91
+ # the token generation as a span
92
+ def generate_token(region:, hostname:, port:, user:, credentials_provider:,
93
+ telemetry_factory: Telemetry::NullTelemetryFactory::INSTANCE)
94
+ telemetry_factory.in_context(TELEMETRY_FETCH_TOKEN, Telemetry::TraceLevel::NESTED, record_failure: true) do
95
+ Aws::RDS::AuthTokenGenerator
96
+ .new(credentials: credentials_provider)
97
+ .auth_token(region:, endpoint: "#{hostname}:#{port}", user_name: user)
98
+ end
90
99
  end
91
100
 
92
101
  def resolve_host(iam_host, host_info)
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'mysql_text'
18
+
17
19
  module AwsAdvancedRubyDriverWrapper
18
20
  module Utils
19
21
  module Parser
@@ -21,21 +23,23 @@ module AwsAdvancedRubyDriverWrapper
21
23
  # Both placeholder styles are accepted, so that the same annotation works for mysql2's
22
24
  # positional +?+ and pg's numbered +$1+.
23
25
  ANNOTATION_PATTERN = %r{/\*@encrypt:([\w.]+)\*/\s*(?:\?|\$(\d+))}
24
- STRIP_PATTERN = %r{/\*@encrypt:[\w.]+\*/\s*}
25
- # The quote characters a literal can be wrapped in. A backtick quotes an identifier rather than
26
- # a value, but is included so a +?+ inside a backtick-quoted name is not miscounted either.
27
- QUOTES = ["'", '"', '`'].freeze
26
+ # Whitespace after an annotation is taken with it, but not a line break, which may be what ends
27
+ # a line comment the annotation sits in.
28
+ STRIP_PATTERN = %r{/\*@encrypt:[\w.]+\*/[^\S\n]*}
28
29
 
29
30
  module_function
30
31
 
31
32
  # Returns a 1-based map of parameter index => "table.column" for each
32
33
  # /*@encrypt:table.column*/ ? or /*@encrypt:table.column*/ $n placeholder.
33
34
  # @param sql [String]
35
+ # @param server_version [Integer, nil] the MySQL server's version, which decides whether a
36
+ # versioned +/*!+ comment is part of the statement (see MysqlText.each_segment)
34
37
  # @return [Hash{Integer => String}]
35
- def parse_annotations(sql)
36
- return {} unless sql.is_a?(String) && !sql.empty?
38
+ def parse_annotations(sql, server_version: nil)
39
+ # Most statements carry no annotation, so they are not scanned for placeholders at all.
40
+ return {} unless annotations?(sql)
37
41
 
38
- question_marks = unquoted_question_mark_positions(sql)
42
+ question_marks = placeholder_positions(sql, server_version)
39
43
 
40
44
  sql.to_enum(:scan, ANNOTATION_PATTERN)
41
45
  .each_with_object({}) do |_, result|
@@ -66,31 +70,11 @@ module AwsAdvancedRubyDriverWrapper
66
70
  end
67
71
 
68
72
  # The character positions of every +?+ that is a bind placeholder, skipping any inside a quoted
69
- # literal so a literal question mark does not shift the count of the placeholder an annotation
70
- # points at. A doubled quote reads as one closing and one opening quote, and a backslash escapes
71
- # the next character except inside a backtick, which matches how the drivers read a literal.
72
- def unquoted_question_mark_positions(sql)
73
+ # literal or a comment so that it does not shift the count of the placeholder an annotation
74
+ # points at. Only mysql2 binds with +?+, so the text is read by MySQL's rules.
75
+ def placeholder_positions(sql, server_version)
73
76
  positions = []
74
- quote = nil
75
- index = 0
76
-
77
- while index < sql.length
78
- char = sql[index]
79
- if quote
80
- if char == '\\' && quote != '`'
81
- index += 2
82
- next
83
- end
84
-
85
- quote = nil if char == quote
86
- elsif QUOTES.include?(char)
87
- quote = char
88
- elsif char == '?'
89
- positions << index
90
- end
91
- index += 1
92
- end
93
-
77
+ MysqlText.each_code_char(sql, server_version: server_version) { |char, index| positions << index if char == '?' }
94
78
  positions
95
79
  end
96
80
  end