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
@@ -0,0 +1,129 @@
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 'mysql_driver_dialect'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module DriverDialects
21
+ # How the mysql2 driver is called. The MySQL grammar and what every MySQL driver shares are
22
+ # inherited.
23
+ class Mysql2DriverDialect < MysqlDriverDialect
24
+ # Every mysql2 call that talks to the server. A call that is not listed here is handed straight
25
+ # to the driver, bypassing the plugin pipeline.
26
+ #
27
+ # Not listed, because libmysql answers them without talking to the server: escape, the row and
28
+ # column counts, last_id, affected_rows, info, warning_count, thread_id, server_info,
29
+ # session_track, and the connection's own settings.
30
+ NETWORK_BOUND_METHODS = (COMMON_NETWORK_BOUND_METHODS | Set[
31
+ RubyMethod::CONNECTION_QUERY.name,
32
+ RubyMethod::CONNECTION_ASYNC_RESULT.name,
33
+ RubyMethod::CONNECTION_SELECT_DB.name,
34
+ RubyMethod::CONNECTION_MORE_RESULTS.name,
35
+ RubyMethod::CONNECTION_NEXT_RESULT.name,
36
+ RubyMethod::CONNECTION_STORE_RESULT.name,
37
+ RubyMethod::CONNECTION_ABANDON_RESULTS.name,
38
+ RubyMethod::CONNECTION_SET_SERVER_OPTION.name,
39
+ RubyMethod::RESULT_EACH.name,
40
+ RubyMethod::RESULT_FREE.name
41
+ ]).freeze
42
+
43
+ def name
44
+ 'mysql2'
45
+ end
46
+
47
+ def connect(host_info, config)
48
+ ::Mysql2::Client.new(**prepare_connect_config(host_info, config))
49
+ end
50
+
51
+ def execute(connection, sql)
52
+ raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
53
+
54
+ connection.query(sql)
55
+ end
56
+
57
+ def execute_with_params(connection, sql, params)
58
+ raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
59
+
60
+ stmt = connection.prepare(sql)
61
+ result = stmt.execute(*params)
62
+ # Everything the caller needs must be read off the statement before the ensure closes its
63
+ # handle. A SELECT result is bound to the handle, so materialize its rows now - otherwise a
64
+ # later iteration (e.g. SqlRunner calling #to_a) raises "Statement handle already closed".
65
+ # A non-SELECT statement returns nil, and its affected-row count must come from the statement
66
+ # too: once the handle is closed the connection-level mysql_affected_rows returns its -1 error
67
+ # sentinel and mysql2 raises. So return that count for #affected_rows to hand back.
68
+ result.is_a?(::Mysql2::Result) ? result.to_a : stmt.affected_rows
69
+ ensure
70
+ stmt&.close
71
+ end
72
+
73
+ # mysql2 binds a blob parameter as raw binary bytes.
74
+ def binary_param(bytes)
75
+ bytes.b
76
+ end
77
+
78
+ # mysql2 hands back a blob column as a string that only needs its encoding forced to binary.
79
+ def read_binary(value)
80
+ value.b
81
+ end
82
+
83
+ # A prepared statement (execute_with_params) hands back its own affected-row count, since the
84
+ # connection-level count is unreadable once the statement handle is closed. A plain
85
+ # connection.query (execute) leaves nil, so the count is read off the connection instead.
86
+ def affected_rows(connection, result)
87
+ result.is_a?(Integer) ? result : connection.affected_rows.to_i
88
+ end
89
+
90
+ # mysql2 has no RETURNING clause, so the generated id is read from the connection afterwards.
91
+ def insert_returning_id(connection, sql, params, _id_column)
92
+ execute_with_params(connection, sql, params)
93
+ id = connection.last_id
94
+ id&.positive? ? id : nil
95
+ end
96
+
97
+ # With automatic_close off, mysql2 invalidates the socket instead of sending COM_QUIT when
98
+ # the client is collected, leaving the parent's session open.
99
+ def abandon_connection(connection)
100
+ return if connection.nil? || connection.closed?
101
+
102
+ connection.automatic_close = false
103
+ rescue StandardError => e
104
+ logger.debug("Failed to abandon MySQL connection: #{e.message}")
105
+ end
106
+
107
+ # mysql2 reports the version it learned when it connected, so reading it costs no round trip.
108
+ def executable_comment_version(connection)
109
+ return nil if connection.nil?
110
+
111
+ info = connection.server_info
112
+ info.is_a?(Hash) ? info[:id] : nil
113
+ rescue StandardError => e
114
+ logger.debug { "The MySQL server version could not be read: #{e.message}" }
115
+ nil
116
+ end
117
+
118
+ def sql_state(exception)
119
+ return nil unless exception.respond_to?(:sql_state)
120
+
121
+ exception.sql_state
122
+ end
123
+
124
+ def network_bound_methods
125
+ NETWORK_BOUND_METHODS
126
+ end
127
+ end
128
+ end
129
+ end
@@ -19,84 +19,22 @@ require_relative '../host/connection_string_host_list_provider'
19
19
 
20
20
  module AwsAdvancedRubyDriverWrapper
21
21
  module DriverDialects
22
+ # What every MySQL driver shares: the server's SQL grammar, and the connection options and
23
+ # connection calls the MySQL drivers name alike. A driver's own dialect subclasses this and adds
24
+ # how the driver itself is called.
22
25
  class MysqlDriverDialect
23
26
  include DriverDialect
24
27
 
25
- # Every mysql2 call that talks to the server. A call that is not listed here is handed straight
26
- # to the driver, bypassing the plugin pipeline.
27
- #
28
- # Not listed, because libmysql answers them without talking to the server: escape, the row and
29
- # column counts, last_id, affected_rows, info, warning_count, thread_id, server_info,
30
- # session_track, and the connection's own settings.
31
- NETWORK_BOUND_METHODS = (COMMON_NETWORK_BOUND_METHODS | Set[
32
- RubyMethod::CONNECTION_QUERY.name,
33
- RubyMethod::CONNECTION_ASYNC_RESULT.name,
34
- RubyMethod::CONNECTION_SELECT_DB.name,
35
- RubyMethod::CONNECTION_MORE_RESULTS.name,
36
- RubyMethod::CONNECTION_NEXT_RESULT.name,
37
- RubyMethod::CONNECTION_STORE_RESULT.name,
38
- RubyMethod::CONNECTION_ABANDON_RESULTS.name,
39
- RubyMethod::CONNECTION_SET_SERVER_OPTION.name,
40
- RubyMethod::RESULT_EACH.name,
41
- RubyMethod::RESULT_FREE.name
42
- ]).freeze
43
-
44
- def connect(host_info, config)
45
- ::Mysql2::Client.new(**prepare_connect_config(host_info, config))
46
- end
47
-
48
- def execute(connection, sql)
49
- raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
50
-
51
- connection.query(sql)
52
- end
53
-
54
- def execute_with_params(connection, sql, params)
55
- raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
56
-
57
- stmt = connection.prepare(sql)
58
- result = stmt.execute(*params)
59
- # Everything the caller needs must be read off the statement before the ensure closes its
60
- # handle. A SELECT result is bound to the handle, so materialize its rows now - otherwise a
61
- # later iteration (e.g. SqlRunner calling #to_a) raises "Statement handle already closed".
62
- # A non-SELECT statement returns nil, and its affected-row count must come from the statement
63
- # too: once the handle is closed the connection-level mysql_affected_rows returns its -1 error
64
- # sentinel and mysql2 raises. So return that count for #affected_rows to hand back.
65
- result.is_a?(::Mysql2::Result) ? result.to_a : stmt.affected_rows
66
- ensure
67
- stmt&.close
28
+ def database_engine
29
+ :mysql
68
30
  end
69
31
 
70
- # mysql2 uses +?+ placeholders, so the SQL is already in its native form.
32
+ # MySQL uses +?+ placeholders, so the SQL is already in its native form.
71
33
  def translate_placeholders(sql)
72
34
  sql
73
35
  end
74
36
 
75
- # mysql2 binds a blob parameter as raw binary bytes.
76
- def binary_param(bytes)
77
- bytes.b
78
- end
79
-
80
- # mysql2 hands back a blob column as a string that only needs its encoding forced to binary.
81
- def read_binary(value)
82
- value.b
83
- end
84
-
85
- # A prepared statement (execute_with_params) hands back its own affected-row count, since the
86
- # connection-level count is unreadable once the statement handle is closed. A plain
87
- # connection.query (execute) leaves nil, so the count is read off the connection instead.
88
- def affected_rows(connection, result)
89
- result.is_a?(Integer) ? result : connection.affected_rows.to_i
90
- end
91
-
92
- # mysql2 has no RETURNING clause, so the generated id is read from the connection afterwards.
93
- def insert_returning_id(connection, sql, params, _id_column)
94
- execute_with_params(connection, sql, params)
95
- id = connection.last_id
96
- id&.positive? ? id : nil
97
- end
98
-
99
- # mysql2 upserts with ON DUPLICATE KEY UPDATE, reading the incoming row from VALUES().
37
+ # MySQL upserts with ON DUPLICATE KEY UPDATE, reading the incoming row from VALUES().
100
38
  def upsert_clause(_conflict_columns, update_columns)
101
39
  assignments = update_columns.map { |column| "#{column} = VALUES(#{column})" }.join(', ')
102
40
  "ON DUPLICATE KEY UPDATE #{assignments}"
@@ -131,26 +69,6 @@ module AwsAdvancedRubyDriverWrapper
131
69
  logger.error("Failed to close MySQL connection: #{e.message}")
132
70
  end
133
71
 
134
- # With automatic_close off, mysql2 invalidates the socket instead of sending COM_QUIT when
135
- # the client is collected, leaving the parent's session open.
136
- def abandon_connection(connection)
137
- return if connection.nil? || connection.closed?
138
-
139
- connection.automatic_close = false
140
- rescue StandardError => e
141
- logger.debug("Failed to abandon MySQL connection: #{e.message}")
142
- end
143
-
144
- def sql_state(exception)
145
- return nil unless exception.respond_to?(:sql_state)
146
-
147
- exception.sql_state
148
- end
149
-
150
- def network_bound_methods
151
- NETWORK_BOUND_METHODS
152
- end
153
-
154
72
  def prepare_connect_config(host_info, config)
155
73
  cfg = {}
156
74
  config.each { |k, v| cfg[k] = v }
@@ -163,9 +81,9 @@ module AwsAdvancedRubyDriverWrapper
163
81
  :username
164
82
  end
165
83
 
84
+ # Read, write and connect timeouts (in seconds) make a query or a close on a dead socket raise a
85
+ # timeout error rather than hang. With mysql2 they also keep it from segfaulting.
166
86
  def apply_monitoring_defaults(driver_props)
167
- # mysql2 read_timeout / write_timeout (in seconds) ensure that queries and closes
168
- # on a dead socket raise Mysql2::Error::TimeoutError instead of segfaulting.
169
87
  driver_props[:read_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
170
88
  driver_props[:write_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
171
89
  driver_props[:connect_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
@@ -85,6 +85,14 @@ module AwsAdvancedRubyDriverWrapper
85
85
  RubyMethod::CONNECTION_LO_TRUNCATE.name
86
86
  ]).freeze
87
87
 
88
+ def name
89
+ 'pg'
90
+ end
91
+
92
+ def database_engine
93
+ :postgresql
94
+ end
95
+
88
96
  def connect(host_info, config)
89
97
  ::PG::Connection.new(**prepare_connect_config(host_info, config))
90
98
  end
@@ -182,6 +190,16 @@ module AwsAdvancedRubyDriverWrapper
182
190
  NETWORK_BOUND_METHODS
183
191
  end
184
192
 
193
+ # libpq separates the socket target (+hostaddr+) from the TLS/IAM server name (+host+), so a pg
194
+ # connection can be pinned to a specific instance IP while still verifying the certificate
195
+ # against - and generating an IAM token for - the original hostname. Blue/Green substitute
196
+ # routing uses this so a pinned connection reaches the intended instance regardless of what DNS
197
+ # resolves to mid-switchover, and still passes +verify-full+ whatever the CA source - instead of
198
+ # connecting to a raw IP (certificate-name mismatch) or falling back to the hostname (DNS risk).
199
+ def props_pinned_to_ip(driver_props, ip)
200
+ driver_props.dup.tap { |props| props[:hostaddr] = ip }
201
+ end
202
+
185
203
  def prepare_connect_config(host_info, config)
186
204
  cfg = {}
187
205
  config.each { |k, v| cfg[k] = v }
@@ -0,0 +1,118 @@
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 'mysql_driver_dialect'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module DriverDialects
21
+ # The trilogy driver speaks the same MySQL protocol and grammar as mysql2, so everything that
22
+ # depends only on the server is inherited. What is overridden is how the driver itself is called.
23
+ class TrilogyDriverDialect < MysqlDriverDialect
24
+ # The trilogy calls that talk to the server and that failover should cover. The failover, Blue/Green
25
+ # and Custom Endpoint plugins subscribe to exactly these, and a call that is not listed here and
26
+ # reaches the client's method_missing is handed straight to the driver.
27
+ #
28
+ # Not listed, because trilogy answers them without talking to the server: escape, closed?,
29
+ # more_results_exist?, the counts and ids of the last statement, server_status, in_transaction?,
30
+ # server_version, the query flags and timeouts, and discard!, which only lets go of the socket.
31
+ # A Trilogy::Result is read off the wire in full before it is returned, so reading its rows
32
+ # never talks to the server either.
33
+ #
34
+ # Not listed although it talks to the server: ping, as for mysql2. ActiveRecord pings to check
35
+ # that a connection is still alive and rescues the driver's own errors to answer false and
36
+ # reconnect; failing over inside that check would raise a wrapper error out of it instead.
37
+ NETWORK_BOUND_METHODS = (COMMON_NETWORK_BOUND_METHODS | Set[
38
+ RubyMethod::CONNECTION_QUERY.name,
39
+ RubyMethod::CONNECTION_CHANGE_DB.name,
40
+ RubyMethod::CONNECTION_SELECT_DB.name,
41
+ RubyMethod::CONNECTION_NEXT_RESULT.name,
42
+ RubyMethod::CONNECTION_ABANDON_RESULTS.name,
43
+ RubyMethod::CONNECTION_SET_SERVER_OPTION.name
44
+ ]).freeze
45
+
46
+ # A placeholder, or a quoted string or identifier, matched whole so that a question mark inside
47
+ # one is not taken for a placeholder. Quotes inside are escaped by a backslash or by doubling.
48
+ PLACEHOLDER_OR_QUOTED = /'(?:[^'\\]|\\.|'')*'|"(?:[^"\\]|\\.|"")*"|`(?:[^`]|``)*`|\?/m
49
+
50
+ def name
51
+ 'trilogy'
52
+ end
53
+
54
+ # Trilogy.new stores the options it is given and writes to them, so it is handed a hash of its
55
+ # own, which prepare_connect_config builds afresh on every call.
56
+ def connect(host_info, config)
57
+ ::Trilogy.new(prepare_connect_config(host_info, config))
58
+ end
59
+
60
+ # Trilogy returns rows as arrays. They are keyed by column name here, as mysql2 returns them,
61
+ # because that is the shape the database dialects read their rows in.
62
+ def execute(connection, sql)
63
+ connection.query(sql).each_hash.to_a
64
+ end
65
+
66
+ # Trilogy has no prepared statements, so each parameter is written into the SQL as a literal,
67
+ # quoted and escaped by the connection the way ActiveRecord quotes its own binds for trilogy.
68
+ # Only the types the wrapper binds in its own queries are accepted.
69
+ def execute_with_params(connection, sql, params)
70
+ placeholders = sql.scan(PLACEHOLDER_OR_QUOTED).count('?')
71
+ raise ArgumentError, "SQL has #{placeholders} placeholders for #{params.size} parameters" unless placeholders == params.size
72
+
73
+ literals = params.map { |value| literal(connection, value) }
74
+ bound_sql = sql.gsub(PLACEHOLDER_OR_QUOTED) { |token| token == '?' ? literals.shift : token }
75
+
76
+ execute(connection, bound_sql)
77
+ end
78
+
79
+ # Every reply from the server carries its status flags, and trilogy keeps the last of them, so
80
+ # whether a transaction is open is known without inferring it from the SQL.
81
+ def reported_in_transaction(connection)
82
+ return nil if closed?(connection)
83
+
84
+ connection.in_transaction?
85
+ end
86
+
87
+ # discard! closes the socket without sending COM_QUIT or a TLS close notify, leaving the
88
+ # parent's session open.
89
+ def abandon_connection(connection)
90
+ return if connection.nil? || connection.closed?
91
+
92
+ connection.discard!
93
+ rescue StandardError => e
94
+ logger.debug("Failed to abandon MySQL connection: #{e.message}")
95
+ end
96
+
97
+ def network_bound_methods
98
+ NETWORK_BOUND_METHODS
99
+ end
100
+
101
+ # trilogy names the TLS CA certificate option +ssl_ca+, where mysql2 names it +sslca+.
102
+ def tls_ca_property_key
103
+ :ssl_ca
104
+ end
105
+
106
+ private
107
+
108
+ def literal(connection, value)
109
+ case value
110
+ when nil then 'NULL'
111
+ when Integer then value.to_s
112
+ when String then "'#{connection.escape(value)}'"
113
+ else raise ArgumentError, "Cannot bind a #{value.class} parameter for trilogy"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -41,6 +41,22 @@ module AwsAdvancedRubyDriverWrapper
41
41
  raise NotImplementedError
42
42
  end
43
43
 
44
+ # Whether the driver reported +error+ as a database error, i.e. it carries a SQLSTATE (or, for
45
+ # trilogy, a MySQL error code). This is not a strict server-vs-client split: mysql2 also reports
46
+ # SQLSTATE HY000 for client-side failures such as a lost connection. Used by the Blue/Green status
47
+ # monitor to tell database errors from other exceptions. A driver whose errors carry no SQLSTATE
48
+ # (trilogy) overrides this to classify by its numeric error code.
49
+ def database_error?(error)
50
+ !@driver_dialect.sql_state(error).nil?
51
+ end
52
+
53
+ # Whether +error+ is a schema/syntax error (SQLSTATE class '42': missing table, unknown column,
54
+ # parse error, missing routine). The Blue/Green status monitor reads this as "the status
55
+ # table/function is not there", i.e. NOT_CREATED. Default is SQLSTATE-based; trilogy overrides.
56
+ def schema_error?(error)
57
+ @driver_dialect.sql_state(error)&.start_with?('42') || false
58
+ end
59
+
44
60
  private
45
61
 
46
62
  def extract_sql_state(error)
@@ -29,7 +29,8 @@ module AwsAdvancedRubyDriverWrapper
29
29
  2006, # CR_SERVER_GONE_ERROR (server has gone away)
30
30
  2013, # CR_SERVER_LOST (lost connection during query)
31
31
  2026, # CR_SSL_CONNECTION_ERROR
32
- 2055 # CR_SERVER_LOST_EXTENDED (lost connection with state info)
32
+ 2055, # CR_SERVER_LOST_EXTENDED (lost connection with state info)
33
+ 4031 # ER_CLIENT_INTERACTION_TIMEOUT (MySQL 8.0.24+ closed a connection that exceeded wait_timeout)
33
34
  ].freeze
34
35
 
35
36
  def initialize(driver_dialect)
@@ -68,12 +69,16 @@ module AwsAdvancedRubyDriverWrapper
68
69
  private
69
70
 
70
71
  def connection_error_network?(error)
71
- return false unless defined?(Mysql2::Error::ConnectionError) && error.is_a?(Mysql2::Error::ConnectionError)
72
+ return false unless defined?(Mysql2::Error) && error.is_a?(Mysql2::Error)
72
73
 
73
- error_number = error.respond_to?(:error_number) ? error.error_number : nil
74
- return true if error_number && NETWORK_ERROR_CODES.include?(error_number)
74
+ NETWORK_ERROR_CODES.include?(error.error_number) || read_timeout?(error)
75
+ end
75
76
 
76
- false
77
+ # mysql2 raises Mysql2::Error::TimeoutError for two unrelated conditions. A client-side read_timeout
78
+ # carries no error number, and mysql2 invalidates the socket before raising it, so the connection is
79
+ # unusable. A lock wait timeout (1205) is a server error on a healthy connection and is excluded.
80
+ def read_timeout?(error)
81
+ error.is_a?(Mysql2::Error::TimeoutError) && error.error_number.nil?
77
82
  end
78
83
  end
79
84
  end
@@ -0,0 +1,127 @@
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 'error_handler'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Errors
21
+ # Trilogy errors carry no SQLSTATE, so they are classified by class and by the server's error code.
22
+ class TrilogyErrorHandler
23
+ include ErrorHandler
24
+
25
+ LOGIN_ERROR_CODES = Set[
26
+ 1044, # ER_DBACCESS_DENIED_ERROR
27
+ 1045 # ER_ACCESS_DENIED_ERROR
28
+ ].freeze
29
+ READ_ONLY_ERROR_CODES = Set[1290, 1836].freeze
30
+ LOCK_WAIT_TIMEOUT_ERROR_CODE = 1205
31
+
32
+ # MySQL error codes whose SQLSTATE is class '42' (schema/syntax). trilogy carries no SQLSTATE,
33
+ # so the Blue/Green monitor's "is the status table/function gone?" question is answered from
34
+ # the code instead - the same codes libmysql would map to a '42xxx' state for mysql2.
35
+ SCHEMA_ERROR_CODES = Set[
36
+ 1051, # ER_BAD_TABLE_ERROR
37
+ 1054, # ER_BAD_FIELD_ERROR
38
+ 1064, # ER_PARSE_ERROR
39
+ 1109, # ER_UNKNOWN_TABLE
40
+ 1142, # ER_TABLEACCESS_DENIED_ERROR
41
+ 1146, # ER_NO_SUCH_TABLE
42
+ 1305, # ER_SP_DOES_NOT_EXIST
43
+ 1370 # ER_PROCACCESS_DENIED_ERROR
44
+ ].freeze
45
+
46
+ # A protocol violation raised while receiving the authentication response. Unlike a clean
47
+ # ER_ACCESS_DENIED_ERROR (1045), trilogy raises this from its C extension as a codeless
48
+ # Trilogy::QueryError (error_code nil, not a ConnectionError), so it is matched by its message
49
+ # marker rather than by code. The server aborting the auth handshake this way means the
50
+ # credential was not accepted, so for login-retry purposes it is a login error.
51
+ AUTH_PROTOCOL_VIOLATION = 'trilogy_auth_recv'
52
+ PROTOCOL_VIOLATION_MARKER = 'TRILOGY_PROTOCOL_VIOLATION'
53
+
54
+ def initialize(driver_dialect)
55
+ @driver_dialect = driver_dialect
56
+ end
57
+
58
+ # Every Trilogy::ConnectionError except those that do not mean the server is unreachable: rejected
59
+ # credentials, a lock wait timeout, an authentication plugin trilogy refuses (an IAM token without
60
+ # enable_cleartext_plugin) and a connection the application closed. Read and write timeouts count,
61
+ # as they do for mysql2.
62
+ def network_error?(error)
63
+ check_cause_chain(error) { |_, current| network_failure?(current) }
64
+ end
65
+
66
+ def network_error_by_sql_state?(_sql_state)
67
+ false
68
+ end
69
+
70
+ def login_error?(error)
71
+ check_cause_chain(error) do |_, current|
72
+ LOGIN_ERROR_CODES.include?(error_code(current)) || auth_protocol_violation?(current)
73
+ end
74
+ end
75
+
76
+ def login_error_by_sql_state?(_sql_state)
77
+ false
78
+ end
79
+
80
+ def read_only_error?(error)
81
+ check_cause_chain(error) { |_, current| READ_ONLY_ERROR_CODES.include?(error_code(current)) }
82
+ end
83
+
84
+ def read_only_error_by_sql_state?(_sql_state, error_code = nil)
85
+ READ_ONLY_ERROR_CODES.include?(error_code)
86
+ end
87
+
88
+ # Whether the driver reported +error+ as a database error. trilogy carries no SQLSTATE, so this
89
+ # classifies by the presence of the server's numeric error code: a code is present for a server
90
+ # error and absent for a client/transport failure (a Trilogy::BaseConnectionError, a read/write
91
+ # timeout, the codeless auth protocol violation).
92
+ def database_error?(error)
93
+ !error_code(error).nil?
94
+ end
95
+
96
+ def schema_error?(error)
97
+ SCHEMA_ERROR_CODES.include?(error_code(error))
98
+ end
99
+
100
+ private
101
+
102
+ def network_failure?(error)
103
+ return false unless defined?(::Trilogy::ConnectionError) && error.is_a?(::Trilogy::ConnectionError)
104
+ return false if error.is_a?(::Trilogy::ConnectionClosed) || error.is_a?(::Trilogy::AuthPluginError)
105
+
106
+ code = error_code(error)
107
+ !LOGIN_ERROR_CODES.include?(code) && code != LOCK_WAIT_TIMEOUT_ERROR_CODE
108
+ end
109
+
110
+ def error_code(error)
111
+ error.respond_to?(:error_code) ? error.error_code : nil
112
+ end
113
+
114
+ # True for a codeless protocol violation raised during the auth handshake. trilogy surfaces a
115
+ # rejected/aborted credential exchange as a Trilogy::QueryError carrying "TRILOGY_PROTOCOL_VIOLATION
116
+ # (trilogy_auth_recv)" with no MySQL error code, so it is identified by message rather than code.
117
+ # Scoped to the auth phase so a mid-session protocol error is not misread as a login failure.
118
+ def auth_protocol_violation?(error)
119
+ return false unless defined?(::Trilogy::Error) && error.is_a?(::Trilogy::Error)
120
+ return false unless error_code(error).nil?
121
+
122
+ message = error.message.to_s
123
+ message.include?(PROTOCOL_VIOLATION_MARKER) && message.include?(AUTH_PROTOCOL_VIOLATION)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -51,6 +51,7 @@ module AwsAdvancedRubyDriverWrapper
51
51
  end
52
52
 
53
53
  class IamAuthError < AwsError; end
54
+ class SamlAuthError < AwsError; end
54
55
  class SecretsManagerAuthError < AwsError; end
55
56
  class PluginConflictError < AwsError; end
56
57
  class BlueGreenTimeoutError < AwsError; end
@@ -506,6 +506,7 @@ module AwsAdvancedRubyDriverWrapper
506
506
  end
507
507
 
508
508
  def stored_hosts
509
+ # Explicitly unregistered: a monitor reading its own data must not keep itself alive.
509
510
  storage_service.get(TOPOLOGY_CACHE_NAME, @cluster_id, register_access: false)
510
511
  end
511
512