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
|
@@ -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
|
|
@@ -131,6 +131,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
131
131
|
@instance_monitors_writer_conn.close
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
def abandon_connections
|
|
135
|
+
@monitoring_connection.abandon
|
|
136
|
+
@instance_monitors_writer_conn.abandon
|
|
137
|
+
driver_dialect = @service_container.dialect_service.driver_dialect
|
|
138
|
+
@instance_monitor_connections.each_pair do |host, conn|
|
|
139
|
+
driver_dialect.abandon_connection(conn)
|
|
140
|
+
@instance_monitor_connections.delete(host)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
134
144
|
private
|
|
135
145
|
|
|
136
146
|
# Main monitoring loop.
|
|
@@ -496,6 +506,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
496
506
|
end
|
|
497
507
|
|
|
498
508
|
def stored_hosts
|
|
509
|
+
# Explicitly unregistered: a monitor reading its own data must not keep itself alive.
|
|
499
510
|
storage_service.get(TOPOLOGY_CACHE_NAME, @cluster_id, register_access: false)
|
|
500
511
|
end
|
|
501
512
|
|
|
@@ -73,6 +73,20 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
73
73
|
|
|
74
74
|
def close; end
|
|
75
75
|
|
|
76
|
+
# Detaches this monitor in a forked child. Its thread did not survive the fork and its
|
|
77
|
+
# connections still belong to the parent, so the thread reference is dropped and the
|
|
78
|
+
# connections are abandoned rather than closed: closing them would end the parent's sessions.
|
|
79
|
+
def release_after_fork
|
|
80
|
+
@stop_flag.make_true
|
|
81
|
+
@thread = nil
|
|
82
|
+
@state.set(MonitorState::STOPPED)
|
|
83
|
+
abandon_connections
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Releases connections inherited from the parent process without closing them on the server.
|
|
87
|
+
# Subclasses that hold connections override this.
|
|
88
|
+
def abandon_connections; end
|
|
89
|
+
|
|
76
90
|
private
|
|
77
91
|
|
|
78
92
|
def run
|
|
@@ -52,6 +52,12 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
52
52
|
def close
|
|
53
53
|
set(nil)
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
# Releases a connection inherited across a fork without closing it on the server. The reference
|
|
57
|
+
# is dropped too, so a later close or set cannot reach the parent's session.
|
|
58
|
+
def abandon
|
|
59
|
+
@driver_dialect.abandon_connection(@connection.get_and_set(nil))
|
|
60
|
+
end
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
end
|
|
@@ -20,6 +20,7 @@ require_relative '../aws_advanced_ruby_driver_wrapper'
|
|
|
20
20
|
require_relative 'utils/connection_config_parser'
|
|
21
21
|
require_relative 'utils/sql_encoding'
|
|
22
22
|
require_relative 'services/service_utility'
|
|
23
|
+
require_relative 'utils/telemetry/telemetry_factory'
|
|
23
24
|
require_relative 'ruby_method'
|
|
24
25
|
require_relative 'errors'
|
|
25
26
|
|
|
@@ -34,13 +35,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
34
35
|
def initialize(**)
|
|
35
36
|
ensure_mysql2!
|
|
36
37
|
config = Utils::ConnectionConfigParser.parse(:mysql2, **)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
telemetry_factory = Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
|
|
39
|
+
telemetry_factory.in_context("#{self.class.name}.new", Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do
|
|
40
|
+
@service_container = Services::ServiceUtility.create_standard_container(config, telemetry_factory:)
|
|
41
|
+
@service_container.host_service.refresh_host_list
|
|
42
|
+
@async_conn = nil
|
|
43
|
+
@async_sql = nil
|
|
44
|
+
@last_sql = nil
|
|
45
|
+
conn_service = @service_container.connection_service
|
|
46
|
+
@service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# Explicitly define critical methods (bypass method_missing to avoid method_missing overhead)
|
|
@@ -53,6 +57,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
53
57
|
# What is remembered is the copy of the SQL that plugins inspect, made once here rather than on
|
|
54
58
|
# every call that is handed it later. The driver is sent the SQL as the application wrote it.
|
|
55
59
|
def query(sql, options = {})
|
|
60
|
+
ensure_connected!
|
|
56
61
|
inspected = Utils::SqlEncoding.inspectable(sql)
|
|
57
62
|
result = pm.execute(RubyMethod::CONNECTION_QUERY, current_conn, ->(*a) { current_conn.query(*a) }, sql, options,
|
|
58
63
|
sql: inspected)
|
|
@@ -76,10 +81,14 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
def ping
|
|
84
|
+
return false if current_conn.nil?
|
|
85
|
+
|
|
79
86
|
pm.execute(RubyMethod::CONNECTION_PING, current_conn, -> { current_conn.ping })
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
def close
|
|
90
|
+
return if current_conn.nil?
|
|
91
|
+
|
|
83
92
|
pm.execute(RubyMethod::CONNECTION_CLOSE, current_conn, -> { current_conn.close })
|
|
84
93
|
end
|
|
85
94
|
|
|
@@ -143,8 +152,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
143
152
|
BOUNDED_TO_ASYNC = Set[:abandon_results!].freeze
|
|
144
153
|
|
|
145
154
|
def method_missing(method_name, *args, **kwargs, &)
|
|
146
|
-
conn =
|
|
147
|
-
raise NoMethodError, 'Connection not initialized' if conn.nil?
|
|
155
|
+
conn = ensure_connected!
|
|
148
156
|
raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
|
|
149
157
|
|
|
150
158
|
method_key = "connection.#{method_name}"
|
|
@@ -197,6 +205,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
197
205
|
@service_container.connection_service.current_connection
|
|
198
206
|
end
|
|
199
207
|
|
|
208
|
+
# Guards the entry points that reach for the current connection directly (the query path and
|
|
209
|
+
# method_missing). Without a live connection AR expects the driver's own "not connected" error,
|
|
210
|
+
# which it translates to a connection error; a bare NoMethodError on nil would not be recognized.
|
|
211
|
+
def ensure_connected!
|
|
212
|
+
conn = current_conn
|
|
213
|
+
raise Mysql2::Error::ConnectionError, 'MySQL client is not connected' if conn.nil?
|
|
214
|
+
|
|
215
|
+
conn
|
|
216
|
+
end
|
|
217
|
+
|
|
200
218
|
def pm
|
|
201
219
|
@service_container.plugin_manager
|
|
202
220
|
end
|
|
@@ -19,6 +19,7 @@ require_relative '../../logging'
|
|
|
19
19
|
require_relative '../../ruby_method'
|
|
20
20
|
require_relative '../../property_definition'
|
|
21
21
|
require_relative '../../utils/rds_utils'
|
|
22
|
+
require_relative 'drop_blue_connections_marker'
|
|
22
23
|
require_relative 'phase'
|
|
23
24
|
require_relative 'role'
|
|
24
25
|
require_relative 'status'
|
|
@@ -32,6 +33,14 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
32
33
|
|
|
33
34
|
BLUE_GREEN_NAME = :blue_green
|
|
34
35
|
|
|
36
|
+
# Storage partition for the drop-blue-connections marker, kept separate from BLUE_GREEN_NAME so
|
|
37
|
+
# it can outlive the status cache's TTL. The marker is stored once per switchover and never
|
|
38
|
+
# re-stamped, so a connection that sits idle longer than the status cache's TTL must still find
|
|
39
|
+
# the marker on its next use. A long TTL is safe: a marker only matches connections opened before
|
|
40
|
+
# its own switchover, and a later switchover overwrites it.
|
|
41
|
+
DROP_MARKER_NAME = :blue_green_drop_marker
|
|
42
|
+
DROP_MARKER_TTL_SEC = 86_400
|
|
43
|
+
|
|
35
44
|
# When present in wrapper_props, internal_connect calls bypass BG routing.
|
|
36
45
|
# Used by BG monitoring connections to avoid being intercepted by their own plugin.
|
|
37
46
|
BG_SKIP_ROUTING_KEY = :'bg.skip_routing'
|
|
@@ -43,6 +52,13 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
43
52
|
|
|
44
53
|
PROVIDERS = Concurrent::Map.new
|
|
45
54
|
|
|
55
|
+
# Storage key, within the DROP_MARKER_NAME cache, under which the drop-blue-connections marker
|
|
56
|
+
# for a deployment is kept. DROP_MARKER_NAME is a separate partition from the status cache, so
|
|
57
|
+
# the marker survives both the status cache's TTL and the provider's reset_context_when_completed.
|
|
58
|
+
def self.drop_marker_key(bgd_id)
|
|
59
|
+
"#{bgd_id}::drop_blue_connections_marker"
|
|
60
|
+
end
|
|
61
|
+
|
|
46
62
|
def initialize(service_container, props = ::Concurrent::Map.new)
|
|
47
63
|
@service_container = service_container
|
|
48
64
|
@wrapper_props = props
|
|
@@ -52,7 +68,17 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
52
68
|
@start_time_ns = Concurrent::AtomicReference.new(0)
|
|
53
69
|
@end_time_ns = Concurrent::AtomicReference.new(0)
|
|
54
70
|
|
|
71
|
+
# Per-connection drop-blue-connections state. Each wrapper client has its own plugin instance,
|
|
72
|
+
# so these describe this connection: when it was opened, the host it was opened to, and the
|
|
73
|
+
# identity of the last marker it was checked against.
|
|
74
|
+
@drop_blue_connections = PropertyDefinition::BG_DROP_BLUE_CONNECTIONS.get_bool(props)
|
|
75
|
+
@connection_open_time_ns = nil
|
|
76
|
+
@connect_time_host = nil
|
|
77
|
+
@checked_drop_marker_id = nil
|
|
78
|
+
@pending_stale_marker_id = nil
|
|
79
|
+
|
|
55
80
|
service_container.storage_service.register(BLUE_GREEN_NAME, ttl: 3600)
|
|
81
|
+
service_container.storage_service.register(DROP_MARKER_NAME, ttl: DROP_MARKER_TTL_SEC)
|
|
56
82
|
|
|
57
83
|
network_methods = service_container.dialect_service.driver_dialect.network_bound_methods
|
|
58
84
|
@subscribed_methods = (Set['connect', 'internal_connect'] | network_methods).freeze
|
|
@@ -61,7 +87,12 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
61
87
|
attr_reader :subscribed_methods
|
|
62
88
|
|
|
63
89
|
def connect(host_info, driver_props, is_initial_connection, pipeline_callable)
|
|
64
|
-
route_connect(host_info, driver_props, is_initial_connection, pipeline_callable, is_internal: false)
|
|
90
|
+
conn = route_connect(host_info, driver_props, is_initial_connection, pipeline_callable, is_internal: false)
|
|
91
|
+
# Only this client's own connect describes its connection. Internal connects (topology
|
|
92
|
+
# monitoring, KMS metadata reads) run through this same plugin instance but open separate
|
|
93
|
+
# connections, so they must not overwrite this connection's open time, host, or marker latches.
|
|
94
|
+
record_connection_open(host_info)
|
|
95
|
+
conn
|
|
65
96
|
end
|
|
66
97
|
|
|
67
98
|
def internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, pipeline_callable)
|
|
@@ -79,7 +110,21 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
79
110
|
|
|
80
111
|
return pipeline_callable.call if CLOSING_METHODS.include?(method_name)
|
|
81
112
|
|
|
113
|
+
# Read the status before the stale check. The provider stores the active drop marker before
|
|
114
|
+
# it writes the POST/COMPLETED status, so reading status first closes the race where a stale
|
|
115
|
+
# check sees a still-pending marker and the COMPLETED early return below then skips re-checking
|
|
116
|
+
# the now-active marker: if the switchover has completed, the check below sees the active marker.
|
|
82
117
|
@bg_status = storage_service.get(BLUE_GREEN_NAME, @bgd_id)
|
|
118
|
+
|
|
119
|
+
# Invalidate a connection opened to the old blue environment before a confirmed switchover,
|
|
120
|
+
# so the failover plugin reconnects through BG routing instead of serving a stale read. This
|
|
121
|
+
# runs before the NOT_CREATED/COMPLETED early return below, because after the switchover the
|
|
122
|
+
# status is COMPLETED (then NOT_CREATED) and would otherwise skip the check entirely.
|
|
123
|
+
if stale_blue_connection?
|
|
124
|
+
invalidate_stale_blue_connection
|
|
125
|
+
return pipeline_callable.call
|
|
126
|
+
end
|
|
127
|
+
|
|
83
128
|
if @bg_status.nil? || @bg_status.current_phase == Phase::NOT_CREATED || @bg_status.current_phase == Phase::COMPLETED
|
|
84
129
|
return pipeline_callable.call
|
|
85
130
|
end
|
|
@@ -95,20 +140,24 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
95
140
|
result = nil
|
|
96
141
|
|
|
97
142
|
while routing && result.nil?
|
|
98
|
-
result = routing.apply(method_name, @wrapper_props, storage_service)
|
|
143
|
+
result = routing.apply(method_name, @wrapper_props, storage_service, telemetry_factory: @service_container.telemetry_factory)
|
|
99
144
|
|
|
100
145
|
next unless result.nil?
|
|
101
146
|
|
|
102
147
|
@bg_status = storage_service.get(BLUE_GREEN_NAME, @bgd_id)
|
|
103
|
-
if @bg_status.nil?
|
|
104
|
-
|
|
105
|
-
return pipeline_callable.call
|
|
106
|
-
end
|
|
148
|
+
break if @bg_status.nil?
|
|
149
|
+
|
|
107
150
|
routing = @bg_status.execute_routing.find { |r| r.match?(current_host, host_role) }
|
|
108
151
|
end
|
|
109
152
|
|
|
110
153
|
@end_time_ns.set(nano_time)
|
|
111
|
-
result.nil?
|
|
154
|
+
return result unless result.nil?
|
|
155
|
+
|
|
156
|
+
# The call may have been held through the switchover (e.g. suspended during IN_PROGRESS and
|
|
157
|
+
# released at POST, which has no execute routing). Re-check whether this connection is now a
|
|
158
|
+
# stale blue connection before running it, so a held call is not run against retired blue.
|
|
159
|
+
invalidate_stale_blue_connection if stale_blue_connection?
|
|
160
|
+
pipeline_callable.call
|
|
112
161
|
ensure
|
|
113
162
|
@end_time_ns.set(nano_time) if @start_time_ns.get.positive? && @end_time_ns.get.zero?
|
|
114
163
|
end
|
|
@@ -130,6 +179,16 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
130
179
|
PROVIDERS.each_key { |k| PROVIDERS.delete(k)&.stop }
|
|
131
180
|
end
|
|
132
181
|
|
|
182
|
+
# Forgets the providers inherited by a forked child so the child starts its own on first use.
|
|
183
|
+
# A provider that fails to release is still forgotten, and the rest are released.
|
|
184
|
+
def self.release_providers_after_fork
|
|
185
|
+
PROVIDERS.each_key do |key|
|
|
186
|
+
PROVIDERS.delete(key)&.release_after_fork
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
AwsAdvancedRubyDriverWrapper.logger.warn("Failed to release Blue/Green status provider #{key} after fork: #{e.message}")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
133
192
|
private
|
|
134
193
|
|
|
135
194
|
def route_connect(host_info, driver_props, is_initial_connection, pipeline_callable, is_internal: false)
|
|
@@ -192,6 +251,65 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
192
251
|
end
|
|
193
252
|
end
|
|
194
253
|
|
|
254
|
+
# Remembers when this connection was opened and the host it was opened to, so a later switchover
|
|
255
|
+
# can tell whether it is a blue connection that predates the switchover.
|
|
256
|
+
def record_connection_open(host_info)
|
|
257
|
+
return unless @drop_blue_connections
|
|
258
|
+
|
|
259
|
+
@connection_open_time_ns = nano_time
|
|
260
|
+
@connect_time_host = host_info&.host&.downcase
|
|
261
|
+
# A (re)connect produces a fresh connection (e.g. after failover): reset the per-connection
|
|
262
|
+
# marker latches so this new incarnation re-evaluates the current marker from a clean slate
|
|
263
|
+
# instead of inheriting the retired connection's checked/pending state.
|
|
264
|
+
@checked_drop_marker_id = nil
|
|
265
|
+
@pending_stale_marker_id = nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Whether this connection should be invalidated because it was opened to the old blue environment
|
|
269
|
+
# before a now-confirmed switchover. The marker is read on every call so a later switchover's
|
|
270
|
+
# marker is noticed. A marker that does not apply to this connection is latched immediately and
|
|
271
|
+
# never re-examined; a marker that does apply is only latched once #invalidate_stale_blue_connection
|
|
272
|
+
# confirms the close, so a transient close failure leaves the connection to be invalidated again on
|
|
273
|
+
# its next use instead of silently serving stale reads for the rest of its life.
|
|
274
|
+
def stale_blue_connection?
|
|
275
|
+
return false unless @drop_blue_connections
|
|
276
|
+
|
|
277
|
+
marker = storage_service.get(DROP_MARKER_NAME, self.class.drop_marker_key(@bgd_id), register_access: false)
|
|
278
|
+
return false unless marker.is_a?(DropBlueConnectionsMarker) && marker.active?
|
|
279
|
+
return false if marker.id == @checked_drop_marker_id
|
|
280
|
+
|
|
281
|
+
if marker.stale?(@connect_time_host, @connection_open_time_ns)
|
|
282
|
+
@pending_stale_marker_id = marker.id
|
|
283
|
+
true
|
|
284
|
+
else
|
|
285
|
+
# This connection does not predate the switchover and never will against this marker (markers
|
|
286
|
+
# are immutable), so latch it now to skip re-reading the marker on every future call.
|
|
287
|
+
@checked_drop_marker_id = marker.id
|
|
288
|
+
false
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Closes the connection directly through the driver dialect rather than the pipeline's close
|
|
293
|
+
# method. Going through the pipeline would set the failover plugin's @closed_explicitly, which
|
|
294
|
+
# would stop it from reconnecting. Closing it out from under the pipeline instead lets the
|
|
295
|
+
# failover plugin (if enabled) see a connection closed that it did not close, fail over, and
|
|
296
|
+
# raise FailoverSuccessError; without failover the driver raises its closed-connection error.
|
|
297
|
+
def invalidate_stale_blue_connection
|
|
298
|
+
logger.info do
|
|
299
|
+
"[bgd_id: '#{@bgd_id}'] Invalidating a connection to #{@connect_time_host} from the old blue environment " \
|
|
300
|
+
'retired by a Blue/Green switchover. It will fail over (or raise a closed-connection error) on this use.'
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
conn = @service_container.connection_service.current_connection
|
|
304
|
+
@service_container.dialect_service.driver_dialect.close_connection(conn) unless conn.nil?
|
|
305
|
+
# Only now that the close is confirmed do we latch the marker as handled. If the close raised
|
|
306
|
+
# and was swallowed below, the marker stays unlatched so the next use retries the invalidation
|
|
307
|
+
# rather than serving a stale read from retired blue for the rest of the connection's life.
|
|
308
|
+
@checked_drop_marker_id = @pending_stale_marker_id
|
|
309
|
+
rescue StandardError => e
|
|
310
|
+
logger.debug { "[bgd_id: '#{@bgd_id}'] Failed to invalidate stale blue connection: #{e.class}: #{e.message}" }
|
|
311
|
+
end
|
|
312
|
+
|
|
195
313
|
def storage_service
|
|
196
314
|
@service_container.storage_service
|
|
197
315
|
end
|