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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -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_action_cable_postgresql_support.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_connection_handler.rb +47 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +23 -9
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +65 -10
- 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 +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +48 -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 -81
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +28 -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 +11 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +14 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +6 -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 +125 -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 +10 -13
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +95 -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 +23 -1
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +49 -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 +23 -4
- 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/events/batching_event_publisher.rb +18 -7
- 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 +29 -8
- 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/{ar_constants.rb → telemetry/open_telemetry_counter.rb} +19 -5
- 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 +47 -2
- metadata +57 -3
|
@@ -17,40 +17,82 @@
|
|
|
17
17
|
require 'active_record/connection_adapters/postgresql_adapter'
|
|
18
18
|
require_relative '../postgresql'
|
|
19
19
|
require_relative '../errors'
|
|
20
|
-
require_relative '
|
|
20
|
+
require_relative 'type_adapter_alias'
|
|
21
|
+
require_relative 'aws_connection_handler'
|
|
22
|
+
require_relative 'aws_action_cable_postgresql_support'
|
|
21
23
|
|
|
22
24
|
module ActiveRecord
|
|
23
25
|
module ConnectionAdapters
|
|
24
26
|
class AwsPostgreSQLAdapter < PostgreSQLAdapter
|
|
25
|
-
ADAPTER_NAME = 'AwsPostgreSQL'
|
|
26
|
-
|
|
27
|
-
# ActiveRecord-only keys that should not be passed to the wrapper or native driver.
|
|
28
|
-
AR_ONLY_KEYS = (AwsAdvancedRubyDriverWrapper::Utils::AR_COMMON_KEYS + %i[advisory_locks schema_search_path]).freeze
|
|
29
|
-
|
|
30
27
|
# ActiveRecord uses :username and :database, but the PG gem expects :user and :dbname.
|
|
31
28
|
# The parent adapter translates these in @connection_parameters, but also strips
|
|
32
29
|
# non-PG keys (including wrapper properties) via slice!. Since we rebuild from @config
|
|
33
30
|
# to preserve wrapper properties, we need to redo this translation ourselves.
|
|
34
31
|
AR_TO_PG_KEY_MAP = { username: :user, database: :dbname }.freeze
|
|
35
32
|
|
|
33
|
+
# The connection options PG accepts, the same set the parent adapter forwards.
|
|
34
|
+
PG_CONNECTION_KEYS = (PG::Connection.conndefaults_hash.keys + [:requiressl]).to_set.freeze
|
|
35
|
+
|
|
36
36
|
def initialize(...)
|
|
37
37
|
# Capture the full config before the parent's initialize strips it
|
|
38
38
|
# down to only PG-recognized keys in @connection_parameters.
|
|
39
39
|
super
|
|
40
40
|
@connection_broken = false
|
|
41
|
-
|
|
42
|
-
wrapper_config = @config.compact.except(*AR_ONLY_KEYS)
|
|
41
|
+
wrapper_config = @config.compact
|
|
43
42
|
# Remap ActiveRecord key names to PG gem key names.
|
|
44
43
|
AR_TO_PG_KEY_MAP.each { |ar_key, pg_key| wrapper_config[pg_key] = wrapper_config.delete(ar_key) if wrapper_config.key?(ar_key) }
|
|
45
|
-
|
|
44
|
+
# Keep only what PG or the wrapper understands. Every other key is an ActiveRecord setting,
|
|
45
|
+
# and PG rejects any connection option it does not recognize.
|
|
46
|
+
@wrapper_config = wrapper_config.select { |key, _| self.class.forwarded_key?(key) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Boolean] whether the config key is passed on to the wrapper connection
|
|
50
|
+
def self.forwarded_key?(key)
|
|
51
|
+
PG_CONNECTION_KEYS.include?(key) ||
|
|
52
|
+
AwsAdvancedRubyDriverWrapper::PropertyDefinition.wrapper_property?(key) ||
|
|
53
|
+
AwsAdvancedRubyDriverWrapper::PropertyDefinition::KNOWN_PREFIXES.any? { |prefix| key.to_s.start_with?(prefix) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolve native database types from the wrapped PostgreSQLAdapter, overriding only :datetime.
|
|
57
|
+
# PostgreSQLAdapter builds its type hash from two things: the shared NATIVE_DATABASE_TYPES
|
|
58
|
+
# constant and the per-class datetime_type class_attribute. Delegating to the parent keeps a
|
|
59
|
+
# single source of truth, but the parent reads *its own* datetime_type, so this adapter's
|
|
60
|
+
# datetime_type (e.g. :timestamptz) must be re-applied here or it is silently ignored and
|
|
61
|
+
# t.datetime columns are created as plain timestamp.
|
|
62
|
+
def self.native_database_types
|
|
63
|
+
superclass.native_database_types.merge(datetime: NATIVE_DATABASE_TYPES[datetime_type])
|
|
46
64
|
end
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
# SQLSTATEs and FATAL-message signals for an authentication/authorization rejection (invalid
|
|
67
|
+
# password, PAM failure, pg_hba.conf rejection). A failed *connect* usually carries no result, so
|
|
68
|
+
# the message text is the reliable signal and the SQLSTATE is only a bonus when one is present.
|
|
69
|
+
AUTH_REJECTION_SQLSTATES = %w[28P01 28000].freeze
|
|
70
|
+
AUTH_REJECTION_MESSAGE = /password authentication failed|PAM authentication failed|pg_hba\.conf/
|
|
49
71
|
|
|
50
72
|
# Note that this config includes wrapper properties.
|
|
73
|
+
#
|
|
74
|
+
# Mirrors the parent PostgreSQLAdapter.new_client so ActiveRecord can tell a missing database
|
|
75
|
+
# (which db:prepare creates) or rejected credentials from other failures.
|
|
51
76
|
def self.new_client(config)
|
|
52
77
|
AwsAdvancedRubyDriverWrapper::WrapperPgConnection.new(**config)
|
|
78
|
+
rescue ::PG::Error => e
|
|
79
|
+
dbname, user, host = config.values_at(:dbname, :user, :host)
|
|
80
|
+
# The postgres maintenance database always exists, so a failure there is not a missing database.
|
|
81
|
+
raise ActiveRecord::ConnectionNotEstablished, e.message if dbname == 'postgres'
|
|
82
|
+
raise ActiveRecord::NoDatabaseError.db_error(dbname) if dbname && e.message.include?(dbname) && !auth_rejection?(e)
|
|
83
|
+
raise ActiveRecord::DatabaseConnectionError.username_error(user) if user && e.message.include?(user)
|
|
84
|
+
raise ActiveRecord::DatabaseConnectionError.hostname_error(host) if host && e.message.include?(host)
|
|
85
|
+
|
|
86
|
+
raise ActiveRecord::ConnectionNotEstablished, e.message
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean] whether the PG error is an authentication/authorization rejection, by SQLSTATE
|
|
90
|
+
# when the error carries a result and otherwise by its canonical FATAL text.
|
|
91
|
+
def self.auth_rejection?(error)
|
|
92
|
+
sqlstate = error.result&.error_field(PG::PG_DIAG_SQLSTATE)
|
|
93
|
+
AUTH_REJECTION_SQLSTATES.include?(sqlstate) || error.message.match?(AUTH_REJECTION_MESSAGE)
|
|
53
94
|
end
|
|
95
|
+
private_class_method :auth_rejection?
|
|
54
96
|
|
|
55
97
|
def connect
|
|
56
98
|
@connection_broken = false
|
|
@@ -71,10 +113,23 @@ module ActiveRecord
|
|
|
71
113
|
super
|
|
72
114
|
end
|
|
73
115
|
|
|
116
|
+
# Used by db:drop, db:reset, db:test:prepare and db:purge. ActiveRecord disconnects its own
|
|
117
|
+
# connections first, but the topology monitor shared by this cluster's connections, and the Blue/Green
|
|
118
|
+
# status providers, keep their own connections to the database, and PostgreSQL refuses to drop a
|
|
119
|
+
# database other sessions are using.
|
|
120
|
+
def drop_database(name)
|
|
121
|
+
raw_connection.stop_topology_monitor
|
|
122
|
+
AwsAdvancedRubyDriverWrapper::Plugins::BlueGreen::BlueGreenPlugin.clean_up_providers
|
|
123
|
+
super
|
|
124
|
+
end
|
|
125
|
+
|
|
74
126
|
def translate_exception(exception, message:, sql:, binds:)
|
|
75
127
|
return super unless exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::AwsError)
|
|
76
128
|
|
|
77
129
|
if exception.needs_reconfiguration
|
|
130
|
+
# The wrapper has reconnected this connection object to a new physical connection, where the
|
|
131
|
+
# statements prepared on the old one do not exist. Forget them so they are prepared again.
|
|
132
|
+
clear_cache!(new_connection: true)
|
|
78
133
|
configure_connection
|
|
79
134
|
exception
|
|
80
135
|
elsif exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::FailoverFailedError)
|
|
@@ -0,0 +1,80 @@
|
|
|
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 'active_record/connection_adapters/trilogy_adapter'
|
|
18
|
+
require_relative '../trilogy'
|
|
19
|
+
require_relative '../errors'
|
|
20
|
+
require_relative 'type_adapter_alias'
|
|
21
|
+
require_relative 'aws_connection_handler'
|
|
22
|
+
|
|
23
|
+
module ActiveRecord
|
|
24
|
+
module ConnectionAdapters
|
|
25
|
+
class AwsTrilogyAdapter < TrilogyAdapter
|
|
26
|
+
# Connects as the parent adapter does, through the wrapper instead of trilogy itself: the ssl_mode
|
|
27
|
+
# name is turned into the constant trilogy reads, and the whole config is passed on, since the
|
|
28
|
+
# wrapper takes its own properties out and trilogy ignores the ActiveRecord-only keys.
|
|
29
|
+
#
|
|
30
|
+
# Translates connection errors the same way the parent adapter does, so ActiveRecord can tell a
|
|
31
|
+
# missing database (which db:prepare creates) or rejected credentials from other failures.
|
|
32
|
+
def self.new_client(config)
|
|
33
|
+
config[:ssl_mode] = parse_ssl_mode(config[:ssl_mode]) if config[:ssl_mode]
|
|
34
|
+
AwsAdvancedRubyDriverWrapper::WrapperTrilogyClient.new(config)
|
|
35
|
+
rescue ::Trilogy::Error => e
|
|
36
|
+
raise translate_connect_error(config, e)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def connect
|
|
40
|
+
@connection_broken = false
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reconnect
|
|
45
|
+
@connection_broken = false
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def active?
|
|
50
|
+
return false if @connection_broken
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def translate_exception(exception, message:, sql:, binds:)
|
|
56
|
+
return super unless exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::AwsError)
|
|
57
|
+
|
|
58
|
+
if exception.needs_reconfiguration
|
|
59
|
+
# The wrapper has reconnected this connection object to a new physical connection, where the
|
|
60
|
+
# session ActiveRecord configured on the old one does not exist. Forget what was cached about
|
|
61
|
+
# that connection and configure the new one.
|
|
62
|
+
clear_cache!(new_connection: true)
|
|
63
|
+
configure_connection
|
|
64
|
+
exception
|
|
65
|
+
elsif exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::FailoverFailedError)
|
|
66
|
+
@connection_broken = true
|
|
67
|
+
ActiveRecord::ConnectionFailed.new(message, sql:, binds:, connection_pool: @pool)
|
|
68
|
+
else
|
|
69
|
+
exception
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
77
|
+
'aws_trilogy',
|
|
78
|
+
'ActiveRecord::ConnectionAdapters::AwsTrilogyAdapter',
|
|
79
|
+
'aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter'
|
|
80
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
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 'active_record'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
# ActiveRecord's adapter-specific type registry (ActiveRecord::Type) keys every type
|
|
21
|
+
# registration and lookup by the *config adapter name*. The wrapped adapters register nothing of
|
|
22
|
+
# their own: they rely entirely on the vanilla PostgreSQL/Mysql2 adapters, which register their
|
|
23
|
+
# OID / native types under adapter: :postgresql and adapter: :mysql2. A lookup for an
|
|
24
|
+
# :aws_postgresql / :aws_mysql2 connection would therefore find none of them and raise
|
|
25
|
+
# "Unknown type" for things like :interval, :point, array/range columns, or MySQL's
|
|
26
|
+
# :unsigned_integer.
|
|
27
|
+
#
|
|
28
|
+
# Rather than copy the parent's registrations (which misses types registered after the adapter
|
|
29
|
+
# loads, needs a duplicate guard, and reaches into the registry's private internals), alias the
|
|
30
|
+
# wrapper adapter names to their parents inside the registry's own register/add_modifier/lookup.
|
|
31
|
+
# Every registration and every lookup — including the ones ActiveRecord routes through
|
|
32
|
+
# Type.adapter_name_from — passes through the registry, so normalizing the adapter there makes the
|
|
33
|
+
# wrapped adapters resolve the exact same types as the vanilla ones, for all current and future
|
|
34
|
+
# types, across Rails 7.2–8.1.
|
|
35
|
+
#
|
|
36
|
+
# Side effect: in an app that uses BOTH a plain postgresql and an aws_postgresql connection, a
|
|
37
|
+
# type explicitly registered for :aws_postgresql is normalized to :postgresql and so also applies
|
|
38
|
+
# to the plain postgresql connections (and vice versa). This is documented in the CHANGELOG.
|
|
39
|
+
module TypeAdapterAlias
|
|
40
|
+
ADAPTER_ALIASES = { aws_postgresql: :postgresql, aws_mysql2: :mysql2, aws_trilogy: :trilogy }.freeze
|
|
41
|
+
|
|
42
|
+
# Prepended onto ActiveRecord::Type::AdapterSpecificRegistry. Normalizing the adapter in lookup
|
|
43
|
+
# covers both the framework path and a direct Type.lookup(..., adapter: :aws_postgresql); any
|
|
44
|
+
# type registered explicitly for a wrapper adapter lands under the parent's key.
|
|
45
|
+
module RegistryAlias
|
|
46
|
+
def register(type_name, klass = nil, adapter: nil, **, &)
|
|
47
|
+
super(type_name, klass, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **, &)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add_modifier(options, klass, adapter: nil, **args)
|
|
51
|
+
super(options, klass, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **args)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def lookup(symbol, *, adapter: nil, **)
|
|
55
|
+
super(symbol, *, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
ActiveRecord::Type::AdapterSpecificRegistry.prepend(AwsAdvancedRubyDriverWrapper::TypeAdapterAlias::RegistryAlias)
|
|
@@ -33,6 +33,17 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
33
33
|
|
|
34
34
|
DEFAULT_MONITORING_TIMEOUT_SEC = 5
|
|
35
35
|
|
|
36
|
+
# @return [String] the name of the Ruby driver gem this dialect drives
|
|
37
|
+
def name
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The database engine the driver talks to, which decides the database dialects that apply.
|
|
42
|
+
# @return [Symbol] +:mysql+ or +:postgresql+
|
|
43
|
+
def database_engine
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
36
47
|
def connect(host_info, config)
|
|
37
48
|
raise NotImplementedError
|
|
38
49
|
end
|
|
@@ -115,6 +126,11 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
115
126
|
raise NotImplementedError
|
|
116
127
|
end
|
|
117
128
|
|
|
129
|
+
# Releases a connection inherited from the parent process after a fork, without telling the
|
|
130
|
+
# server: the parent still owns the session. Garbage collection or process exit in the child
|
|
131
|
+
# must not end it.
|
|
132
|
+
def abandon_connection(connection); end
|
|
133
|
+
|
|
118
134
|
def sql_state(_exception)
|
|
119
135
|
nil
|
|
120
136
|
end
|
|
@@ -133,6 +149,29 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
133
149
|
:user
|
|
134
150
|
end
|
|
135
151
|
|
|
152
|
+
# Returns the property key the underlying driver expects for the TLS CA certificate file.
|
|
153
|
+
# Used by Blue/Green substitute routing for drivers that overload the host for both the socket
|
|
154
|
+
# address and the TLS server name (mysql2, trilogy): it tells whether TLS is active before
|
|
155
|
+
# deciding to connect by hostname rather than raw IP, avoiding a certificate-name mismatch.
|
|
156
|
+
# Drivers that can pin the socket separately (see #props_pinned_to_ip) do not use this.
|
|
157
|
+
# mysql2 names it +:sslca+ (the default); override where it differs (trilogy uses +:ssl_ca+).
|
|
158
|
+
def tls_ca_property_key
|
|
159
|
+
:sslca
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Blue/Green substitute routing must connect to a specific instance IP — so a DNS lookup during
|
|
163
|
+
# the switchover window cannot resolve to the old instance — while still presenting the original
|
|
164
|
+
# hostname for TLS certificate verification and IAM token generation. A driver that can pin the
|
|
165
|
+
# socket address separately from the TLS/auth server name returns a copy of +driver_props+ that
|
|
166
|
+
# connects to +ip+ but keeps the hostname for verification; a driver that overloads the host for
|
|
167
|
+
# both returns +nil+, and the routing falls back to the #tls_ca_property_key hostname check.
|
|
168
|
+
# @param driver_props [Hash]
|
|
169
|
+
# @param ip [String] the instance IP to pin the socket to
|
|
170
|
+
# @return [Hash, nil] props pinned to +ip+, or +nil+ when the driver cannot split socket from TLS name
|
|
171
|
+
def props_pinned_to_ip(_driver_props, _ip)
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
136
175
|
# Applies default socket/connect timeouts to monitoring connection driver props.
|
|
137
176
|
# These ensure that a query or close on a dead connection raises a timeout error
|
|
138
177
|
# rather than hanging indefinitely or segfaulting.
|
|
@@ -149,6 +188,15 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
149
188
|
def reported_in_transaction(_connection)
|
|
150
189
|
nil
|
|
151
190
|
end
|
|
191
|
+
|
|
192
|
+
# The server version that decides which versioned /*!80023 ... */ executable comments the server
|
|
193
|
+
# runs, as MySQL numbers it (8.0.39 is 80039). Only MySQL has executable comments.
|
|
194
|
+
# @param connection [Object]
|
|
195
|
+
# @return [Integer, nil] nil when the driver's SQL has no executable comments, or the version
|
|
196
|
+
# cannot be read
|
|
197
|
+
def executable_comment_version(_connection)
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
152
200
|
end
|
|
153
201
|
end
|
|
154
202
|
end
|
|
@@ -14,26 +14,33 @@
|
|
|
14
14
|
# See the License for the specific language governing permissions and
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
|
-
require_relative '
|
|
17
|
+
require_relative 'mysql2_driver_dialect'
|
|
18
18
|
require_relative 'pg_driver_dialect'
|
|
19
|
+
require_relative 'trilogy_driver_dialect'
|
|
19
20
|
require_relative '../errors'
|
|
20
21
|
require_relative '../errors/mysql_error_handler'
|
|
21
22
|
require_relative '../errors/pg_error_handler'
|
|
23
|
+
require_relative '../errors/trilogy_error_handler'
|
|
22
24
|
|
|
23
25
|
module AwsAdvancedRubyDriverWrapper
|
|
24
26
|
module DriverDialects
|
|
25
27
|
module DriverDialectManager
|
|
26
|
-
|
|
28
|
+
MYSQL2_DIALECT = Mysql2DriverDialect.new.freeze
|
|
27
29
|
PG_DIALECT = PgDriverDialect.new.freeze
|
|
30
|
+
TRILOGY_DIALECT = TrilogyDriverDialect.new.freeze
|
|
28
31
|
|
|
29
32
|
REGISTRY = {
|
|
30
33
|
mysql2: {
|
|
31
|
-
driver_dialect:
|
|
32
|
-
error_handler: Errors::MysqlErrorHandler.new(
|
|
34
|
+
driver_dialect: MYSQL2_DIALECT,
|
|
35
|
+
error_handler: Errors::MysqlErrorHandler.new(MYSQL2_DIALECT)
|
|
33
36
|
},
|
|
34
37
|
postgresql: {
|
|
35
38
|
driver_dialect: PG_DIALECT,
|
|
36
39
|
error_handler: Errors::PgErrorHandler.new(PG_DIALECT)
|
|
40
|
+
},
|
|
41
|
+
trilogy: {
|
|
42
|
+
driver_dialect: TRILOGY_DIALECT,
|
|
43
|
+
error_handler: Errors::TrilogyErrorHandler.new(TRILOGY_DIALECT)
|
|
37
44
|
}
|
|
38
45
|
}.freeze
|
|
39
46
|
|
|
@@ -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,16 +69,6 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
131
69
|
logger.error("Failed to close MySQL connection: #{e.message}")
|
|
132
70
|
end
|
|
133
71
|
|
|
134
|
-
def sql_state(exception)
|
|
135
|
-
return nil unless exception.respond_to?(:sql_state)
|
|
136
|
-
|
|
137
|
-
exception.sql_state
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def network_bound_methods
|
|
141
|
-
NETWORK_BOUND_METHODS
|
|
142
|
-
end
|
|
143
|
-
|
|
144
72
|
def prepare_connect_config(host_info, config)
|
|
145
73
|
cfg = {}
|
|
146
74
|
config.each { |k, v| cfg[k] = v }
|
|
@@ -153,9 +81,9 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
153
81
|
:username
|
|
154
82
|
end
|
|
155
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.
|
|
156
86
|
def apply_monitoring_defaults(driver_props)
|
|
157
|
-
# mysql2 read_timeout / write_timeout (in seconds) ensure that queries and closes
|
|
158
|
-
# on a dead socket raise Mysql2::Error::TimeoutError instead of segfaulting.
|
|
159
87
|
driver_props[:read_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
|
|
160
88
|
driver_props[:write_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
|
|
161
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
|
|
@@ -162,6 +170,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
162
170
|
logger.error("Failed to close PostgreSQL connection: #{e.message}")
|
|
163
171
|
end
|
|
164
172
|
|
|
173
|
+
# Points the socket at /dev/null, so the Terminate message libpq sends when the connection
|
|
174
|
+
# is finished or collected never reaches the server.
|
|
175
|
+
def abandon_connection(connection)
|
|
176
|
+
return if connection.nil? || connection.finished?
|
|
177
|
+
|
|
178
|
+
connection.socket_io.reopen(IO::NULL)
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
logger.debug("Failed to abandon PostgreSQL connection: #{e.message}")
|
|
181
|
+
end
|
|
182
|
+
|
|
165
183
|
def sql_state(exception)
|
|
166
184
|
return nil unless exception.is_a?(::PG::Error) && exception.result
|
|
167
185
|
|
|
@@ -172,6 +190,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
172
190
|
NETWORK_BOUND_METHODS
|
|
173
191
|
end
|
|
174
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
|
+
|
|
175
203
|
def prepare_connect_config(host_info, config)
|
|
176
204
|
cfg = {}
|
|
177
205
|
config.each { |k, v| cfg[k] = v }
|