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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -2
- data/README.md +115 -6
- data/THIRD-PARTY-LICENSES +70 -11
- data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +17 -3
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
- data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
- 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
|
-
|
|
26
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
72
|
+
return false unless defined?(Mysql2::Error) && error.is_a?(Mysql2::Error)
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
NETWORK_ERROR_CODES.include?(error.error_number) || read_timeout?(error)
|
|
75
|
+
end
|
|
75
76
|
|
|
76
|
-
|
|
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
|