aws_advanced_ruby_driver_wrapper 1.0.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +175 -0
- data/NOTICE +1 -0
- data/README.md +168 -0
- data/THIRD-PARTY-LICENSES +473 -0
- data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
- data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
- data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
- data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
- metadata +227 -0
|
@@ -0,0 +1,153 @@
|
|
|
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 '../host/host_role'
|
|
18
|
+
require_relative '../host/host_availability'
|
|
19
|
+
require_relative '../logging'
|
|
20
|
+
|
|
21
|
+
module AwsAdvancedRubyDriverWrapper
|
|
22
|
+
module Utils
|
|
23
|
+
class RetryUtil
|
|
24
|
+
include Logging
|
|
25
|
+
|
|
26
|
+
SHORT_DELAY_SEC = 0.1
|
|
27
|
+
DEFAULT_STRATEGY = 'random'
|
|
28
|
+
|
|
29
|
+
Result = Data.define(:connection, :host_info)
|
|
30
|
+
|
|
31
|
+
def initialize(service_container)
|
|
32
|
+
@host_service = service_container.host_service
|
|
33
|
+
@dialect_service = service_container.dialect_service
|
|
34
|
+
@connection_service = service_container.connection_service
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Repeatedly refreshes the topology and attempts to open a connection to the current writer,
|
|
38
|
+
# until one is established or the deadline passes.
|
|
39
|
+
#
|
|
40
|
+
# @param plugin_to_skip [Object] the plugin that should be skipped in the connect pipeline
|
|
41
|
+
# @param plugin_manager [Services::PluginManager]
|
|
42
|
+
# @param deadline [Time] the point in time at which to give up
|
|
43
|
+
# @return [Result] the new connection and the host it was opened to
|
|
44
|
+
# @raise [Timeout::Error] if no connection could be established before the deadline
|
|
45
|
+
def connect_to_writer(plugin_to_skip, plugin_manager, deadline:)
|
|
46
|
+
connect_to_allowed_host(plugin_to_skip, plugin_manager, verify_role: Host::HostRole::WRITER, deadline: deadline) do |allowed_hosts|
|
|
47
|
+
writer_candidate = @host_service.all_hosts.find { |h| h.role == Host::HostRole::WRITER }
|
|
48
|
+
|
|
49
|
+
if writer_candidate.nil?
|
|
50
|
+
logger.debug { 'No writer host found in topology' }
|
|
51
|
+
nil
|
|
52
|
+
elsif allowed_hosts.none? { |h| h.host_and_port == writer_candidate.host_and_port }
|
|
53
|
+
logger.debug { "New writer not in allowed hosts: #{writer_candidate.url}" }
|
|
54
|
+
nil
|
|
55
|
+
else
|
|
56
|
+
[writer_candidate]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Repeatedly refreshes the topology and attempts to open a connection to one of the hosts
|
|
62
|
+
# selected by the given block, until one is established or the deadline passes.
|
|
63
|
+
#
|
|
64
|
+
# Hosts that cannot be connected to, or whose role does not match +verify_role+, are dropped
|
|
65
|
+
# from the candidate list. Once every candidate has been ruled out, the topology is refreshed
|
|
66
|
+
# and the block is consulted again.
|
|
67
|
+
#
|
|
68
|
+
# @param plugin_to_skip [Object] the plugin that should be skipped in the connect pipeline
|
|
69
|
+
# @param plugin_manager [Services::PluginManager]
|
|
70
|
+
# @param deadline [Time] the point in time at which to give up
|
|
71
|
+
# @param verify_role [Symbol, nil] the role the new connection must report, or nil to accept any host
|
|
72
|
+
# @param strategy [String] the host selection strategy used to order the candidates
|
|
73
|
+
# @yieldparam allowed_hosts [Array<Host::HostInfo>] the current allowed hosts
|
|
74
|
+
# @yieldreturn [Array<Host::HostInfo>, nil] the hosts to attempt, or nil/empty to refresh and retry
|
|
75
|
+
# @return [Result] the new connection and the host it was opened to
|
|
76
|
+
# @raise [Timeout::Error] if no connection could be established before the deadline
|
|
77
|
+
def connect_to_allowed_host(plugin_to_skip, plugin_manager, deadline:, verify_role: nil, strategy: DEFAULT_STRATEGY)
|
|
78
|
+
strategy = DEFAULT_STRATEGY if strategy.nil? || strategy.to_s.empty?
|
|
79
|
+
|
|
80
|
+
loop do
|
|
81
|
+
break if Time.now >= deadline
|
|
82
|
+
|
|
83
|
+
# The roles in this list might not be accurate, depending on whether the new topology has become available yet.
|
|
84
|
+
@host_service.refresh_host_list
|
|
85
|
+
candidates = yield(@host_service.hosts)
|
|
86
|
+
|
|
87
|
+
if candidates.nil? || candidates.empty?
|
|
88
|
+
sleep(SHORT_DELAY_SEC)
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Copy the candidates and mark them available so that the selection strategy considers all of them.
|
|
93
|
+
remaining = candidates.map do |host|
|
|
94
|
+
host.deep_dup.tap { |dup| dup.availability = Host::HostAvailability::AVAILABLE }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
while !remaining.empty? && Time.now < deadline
|
|
98
|
+
candidate = select_candidate(remaining, verify_role, strategy)
|
|
99
|
+
if candidate.nil?
|
|
100
|
+
logger.debug { "Unable to find #{verify_role || 'a host'} in the updated host list: #{remaining.map(&:url)}" }
|
|
101
|
+
sleep(SHORT_DELAY_SEC)
|
|
102
|
+
break # Give up on this candidate list and refresh the topology.
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result = attempt_connection(candidate, verify_role, plugin_to_skip, plugin_manager)
|
|
106
|
+
return result if result
|
|
107
|
+
|
|
108
|
+
remaining.delete(candidate)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
raise Timeout::Error, 'Not able to establish a connection before timing out'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# @return [Result, nil] the result, or nil if the connection failed or reported the wrong role
|
|
118
|
+
def attempt_connection(candidate, verify_role, plugin_to_skip, plugin_manager)
|
|
119
|
+
conn = plugin_manager.connect(candidate, @connection_service.driver_props, false, plugin_to_skip: plugin_to_skip)
|
|
120
|
+
|
|
121
|
+
# Since the roles in the host list might not be accurate, we execute a query to check the instance's role.
|
|
122
|
+
role = verify_role.nil? ? nil : @dialect_service.db_dialect.host_role(conn)
|
|
123
|
+
if verify_role.nil?
|
|
124
|
+
return Result.new(conn, candidate)
|
|
125
|
+
elsif verify_role == role
|
|
126
|
+
return Result.new(conn, candidate.deep_dup(role: role))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The role is not the one that was asked for, so the connection is not valid.
|
|
130
|
+
close_quietly(conn)
|
|
131
|
+
nil
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
logger.debug { "Exception connecting to #{candidate.host}: #{e.message}" }
|
|
134
|
+
close_quietly(conn)
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def select_candidate(hosts, role, strategy)
|
|
139
|
+
@host_service.select_host(hosts, role, strategy)
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def close_quietly(conn)
|
|
145
|
+
return if conn.nil?
|
|
146
|
+
|
|
147
|
+
@dialect_service.driver_dialect.close_connection(conn)
|
|
148
|
+
rescue StandardError
|
|
149
|
+
# ignore
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
17
|
+
module Utils
|
|
18
|
+
module SqlEncoding
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# A copy of the SQL as valid UTF-8, which is what the patterns and parsers that inspect statements
|
|
22
|
+
# can read. A statement in an encoding that is not ASCII-compatible, such as UTF-16, cannot be
|
|
23
|
+
# matched against a UTF-8 pattern at all, and neither can one with bytes that are invalid in its
|
|
24
|
+
# encoding.
|
|
25
|
+
#
|
|
26
|
+
# Only the copy is converted. The SQL a driver is sent is the caller's own, since the driver
|
|
27
|
+
# converts it to the connection's encoding itself.
|
|
28
|
+
#
|
|
29
|
+
# A character with no UTF-8 equivalent, or a byte that is invalid, is replaced rather than raised on,
|
|
30
|
+
# so that inspecting a statement never fails a call the driver would have made.
|
|
31
|
+
#
|
|
32
|
+
# SQL that is binary, or in one of the few encodings Ruby has no converter to UTF-8 for, has no
|
|
33
|
+
# text to convert. The driver sends such SQL as the bytes it is, so its bytes are read as UTF-8,
|
|
34
|
+
# which is how the server reads them on a UTF-8 connection. Replacing or skipping them instead
|
|
35
|
+
# would leave the checks reading a different statement from the one the server runs.
|
|
36
|
+
#
|
|
37
|
+
# @param sql [Object] the SQL to inspect
|
|
38
|
+
# @return [String, Object] the SQL as valid UTF-8, or anything that is not a String as it is
|
|
39
|
+
def inspectable(sql)
|
|
40
|
+
return sql unless sql.is_a?(String)
|
|
41
|
+
return sql.valid_encoding? ? sql : sql.scrub if sql.encoding == Encoding::UTF_8
|
|
42
|
+
return sql if sql.ascii_only? && sql.encoding.ascii_compatible?
|
|
43
|
+
return bytes_as_utf8(sql) if sql.encoding == Encoding::BINARY
|
|
44
|
+
|
|
45
|
+
sql.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
46
|
+
rescue Encoding::ConverterNotFoundError
|
|
47
|
+
bytes_as_utf8(sql)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def bytes_as_utf8(sql)
|
|
51
|
+
sql.b.force_encoding(Encoding::UTF_8).scrub
|
|
52
|
+
end
|
|
53
|
+
private_class_method :bytes_as_utf8
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
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 '../ruby_method'
|
|
18
|
+
require_relative 'sql_encoding'
|
|
19
|
+
|
|
20
|
+
module AwsAdvancedRubyDriverWrapper
|
|
21
|
+
module Utils
|
|
22
|
+
module SqlMethodAnalyzer
|
|
23
|
+
EXECUTE_SQL_METHODS = Set[
|
|
24
|
+
RubyMethod::CONNECTION_EXEC.name,
|
|
25
|
+
RubyMethod::CONNECTION_ASYNC_EXEC.name,
|
|
26
|
+
RubyMethod::CONNECTION_EXEC_PARAMS.name,
|
|
27
|
+
RubyMethod::CONNECTION_EXEC_PREPARED.name,
|
|
28
|
+
RubyMethod::CONNECTION_QUERY.name,
|
|
29
|
+
RubyMethod::CONNECTION_SEND_QUERY.name,
|
|
30
|
+
RubyMethod::CONNECTION_SEND_QUERY_PARAMS.name,
|
|
31
|
+
RubyMethod::STATEMENT_EXECUTE.name
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
CLOSE_TRANSACTION_METHODS = Set[
|
|
35
|
+
RubyMethod::CONNECTION_CLOSE.name
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
QUOTE_CHARS = ["'", '"', '`'].freeze
|
|
39
|
+
|
|
40
|
+
# The combined transaction-state decision for one executed statement. +autocommit_value+ is nil
|
|
41
|
+
# when the statement does not set autocommit.
|
|
42
|
+
TransactionEffect = Data.define(:opens_transaction, :closes_transaction, :autocommit_value)
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Computes every transaction-state effect of a statement in a single pass. It runs on every
|
|
47
|
+
# executed statement, so the SQL is normalized once here rather than once per question asked
|
|
48
|
+
# of it (open? close? set autocommit?).
|
|
49
|
+
def transaction_effect(method_name, args, autocommit:, autocommit_before:, mysql_backslash_escapes: false)
|
|
50
|
+
method_closes = CLOSE_TRANSACTION_METHODS.include?(method_name) ||
|
|
51
|
+
method_name == RubyMethod::CONNECTION_TRANSACTION.name
|
|
52
|
+
|
|
53
|
+
unless EXECUTE_SQL_METHODS.include?(method_name)
|
|
54
|
+
return TransactionEffect.new(opens_transaction: false, closes_transaction: method_closes, autocommit_value: nil)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
stmt = first_statement(args&.first, mysql_backslash_escapes: mysql_backslash_escapes)
|
|
58
|
+
return TransactionEffect.new(opens_transaction: false, closes_transaction: method_closes, autocommit_value: nil) unless stmt
|
|
59
|
+
|
|
60
|
+
sets_autocommit = stmt.start_with?('SET AUTOCOMMIT')
|
|
61
|
+
autocommit_value = sets_autocommit ? parse_autocommit_value(stmt) : nil
|
|
62
|
+
|
|
63
|
+
opens = starts_transaction?(stmt) || (!autocommit && opens_transaction_scope?(stmt))
|
|
64
|
+
closes = method_closes || ends_transaction?(stmt) ||
|
|
65
|
+
(!autocommit_before && sets_autocommit && autocommit_value == true)
|
|
66
|
+
|
|
67
|
+
TransactionEffect.new(opens_transaction: opens, closes_transaction: closes, autocommit_value: autocommit_value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extracts the boolean autocommit value from an already-normalized statement.
|
|
71
|
+
def parse_autocommit_value(stmt)
|
|
72
|
+
sep = stmt.index('=')
|
|
73
|
+
if sep
|
|
74
|
+
val_start = sep + 1
|
|
75
|
+
else
|
|
76
|
+
to_idx = stmt.index(' TO ')
|
|
77
|
+
return nil unless to_idx
|
|
78
|
+
|
|
79
|
+
val_start = to_idx + 4
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
val = stmt[val_start..].split(';', 2).first.strip
|
|
83
|
+
case val
|
|
84
|
+
when 'TRUE', '1', 'ON' then true
|
|
85
|
+
when 'FALSE', '0', 'OFF' then false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def first_statement(sql, mysql_backslash_escapes: false)
|
|
90
|
+
sql = SqlEncoding.inspectable(sql)
|
|
91
|
+
return nil unless sql.is_a?(String) && !sql.strip.empty?
|
|
92
|
+
|
|
93
|
+
stmts = strip_comments(sql, mysql_backslash_escapes: mysql_backslash_escapes).split(';')
|
|
94
|
+
return nil if stmts.empty?
|
|
95
|
+
|
|
96
|
+
stmts.first.squeeze(' ').strip.upcase(:ascii)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Removes -- line comments, # line comments (MySQL), and /* */ block comments.
|
|
100
|
+
# Quoted sections ('', "", ``, $$…$$) are preserved so comment markers inside strings are ignored.
|
|
101
|
+
# Postgres nested block comments (/* /* */ */) are handled correctly.
|
|
102
|
+
# Pass mysql_backslash_escapes: true to honour \ as an escape inside quoted strings.
|
|
103
|
+
# Each comment is replaced by a single space to avoid merging adjacent tokens.
|
|
104
|
+
def strip_comments(sql, mysql_backslash_escapes: false)
|
|
105
|
+
result = +''
|
|
106
|
+
i = 0
|
|
107
|
+
len = sql.length
|
|
108
|
+
while i < len
|
|
109
|
+
c = sql[i]
|
|
110
|
+
if c == '$' && (i + 1 < len) && (sql[i + 1] == '$' || sql[i + 1].match?(/[A-Za-z_]/)) &&
|
|
111
|
+
(m = sql[i..].match(/\A(\$[^$]*\$)/n))
|
|
112
|
+
# Postgres dollar-quoted string: $tag$...$tag$
|
|
113
|
+
tag = m[1]
|
|
114
|
+
close = sql.index(tag, i + tag.length)
|
|
115
|
+
if close.nil?
|
|
116
|
+
result << sql[i..]
|
|
117
|
+
i = len
|
|
118
|
+
else
|
|
119
|
+
result << sql[i...(close + tag.length)]
|
|
120
|
+
i = close + tag.length
|
|
121
|
+
end
|
|
122
|
+
elsif QUOTE_CHARS.include?(c)
|
|
123
|
+
j = skip_quoted(sql, i, backslash_escapes: mysql_backslash_escapes)
|
|
124
|
+
result << sql[i...j]
|
|
125
|
+
i = j
|
|
126
|
+
elsif c == '#' || (c == '-' && i + 1 < len && sql[i + 1] == '-')
|
|
127
|
+
i = skip_to_end_of_line(sql, i)
|
|
128
|
+
result << ' '
|
|
129
|
+
elsif c == '/' && i + 1 < len && sql[i + 1] == '*'
|
|
130
|
+
i = skip_block_comment(sql, i + 2)
|
|
131
|
+
result << ' '
|
|
132
|
+
else
|
|
133
|
+
result << c
|
|
134
|
+
i += 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
result
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def skip_block_comment(sql, start)
|
|
141
|
+
depth = 1
|
|
142
|
+
i = start
|
|
143
|
+
len = sql.length
|
|
144
|
+
while i < len && depth.positive?
|
|
145
|
+
if sql[i] == '/' && sql[i + 1] == '*'
|
|
146
|
+
depth += 1
|
|
147
|
+
i += 2
|
|
148
|
+
elsif sql[i] == '*' && sql[i + 1] == '/'
|
|
149
|
+
depth -= 1
|
|
150
|
+
i += 2
|
|
151
|
+
else
|
|
152
|
+
i += 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
i
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def skip_quoted(sql, start, backslash_escapes: false)
|
|
159
|
+
quote = sql[start]
|
|
160
|
+
i = start + 1
|
|
161
|
+
while i < sql.length
|
|
162
|
+
if backslash_escapes && sql[i] == '\\'
|
|
163
|
+
i += 2
|
|
164
|
+
elsif sql[i] == quote
|
|
165
|
+
return i + 1 if i + 1 >= sql.length || sql[i + 1] != quote
|
|
166
|
+
|
|
167
|
+
i += 2
|
|
168
|
+
else
|
|
169
|
+
i += 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
sql.length
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def skip_to_end_of_line(sql, start)
|
|
176
|
+
i = start
|
|
177
|
+
i += 1 while i < sql.length && sql[i] != "\n" && sql[i] != "\r"
|
|
178
|
+
i
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def starts_transaction?(stmt)
|
|
182
|
+
stmt.start_with?('BEGIN', 'START TRANSACTION')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ends_transaction?(stmt)
|
|
186
|
+
stmt.start_with?('COMMIT', 'ROLLBACK', 'END', 'ABORT')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def opens_transaction_scope?(stmt)
|
|
190
|
+
!starts_transaction?(stmt) && !ends_transaction?(stmt) &&
|
|
191
|
+
!stmt.start_with?('SET ') && !stmt.start_with?('USE ') && !stmt.start_with?('SHOW ')
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Storage
|
|
20
|
+
# A container that holds a cached value along with its expiration time.
|
|
21
|
+
class CacheEntry
|
|
22
|
+
attr_reader :value
|
|
23
|
+
|
|
24
|
+
# @param value [Object] the cached value.
|
|
25
|
+
# @param expiration_time [Float] monotonic clock time (in seconds) at which this entry expires.
|
|
26
|
+
def initialize(value, expiration_time)
|
|
27
|
+
@value = value
|
|
28
|
+
@expiration_time = expiration_time
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] true if the entry has passed its expiration time.
|
|
32
|
+
def expired?
|
|
33
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > @expiration_time
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Extends the expiration by the given TTL from the current time.
|
|
37
|
+
# @param ttl [Numeric] time-to-live in seconds from now.
|
|
38
|
+
def extend_expiration(ttl)
|
|
39
|
+
@expiration_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + ttl
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param other [Object] the object to compare.
|
|
43
|
+
# @return [Boolean] true if both entries hold the same value.
|
|
44
|
+
def ==(other)
|
|
45
|
+
other.is_a?(self.class) && @value == other.value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias eql? ==
|
|
49
|
+
|
|
50
|
+
def hash
|
|
51
|
+
value.hash
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
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 'cache_entry'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
module Utils
|
|
21
|
+
module Storage
|
|
22
|
+
# A thread-safe cache with fixed TTL expiration. Entries expire after a set duration
|
|
23
|
+
# and are not renewed on access. Expired entries are removed on read or via
|
|
24
|
+
# {#remove_expired_entries}. Suitable for simple data that does not require
|
|
25
|
+
# special cleanup when removed.
|
|
26
|
+
#
|
|
27
|
+
# For entries that need sliding expiration or controlled removal,
|
|
28
|
+
# see {SlidingExpirationCache}.
|
|
29
|
+
class ExpirationCache
|
|
30
|
+
DEFAULT_TTL = 300 # 5 minutes in seconds
|
|
31
|
+
|
|
32
|
+
# @param ttl [Numeric] time-to-live for cache entries in seconds.
|
|
33
|
+
def initialize(ttl: DEFAULT_TTL)
|
|
34
|
+
@cache = {}
|
|
35
|
+
@ttl = ttl
|
|
36
|
+
@lock = Mutex.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Stores the given value at the given key.
|
|
40
|
+
# @param key [Object] the cache key.
|
|
41
|
+
# @param value [Object] the value to store.
|
|
42
|
+
# @return [Object, nil] the previous value, or nil.
|
|
43
|
+
def put(key, value)
|
|
44
|
+
@lock.synchronize do
|
|
45
|
+
previous = @cache[key]
|
|
46
|
+
@cache[key] = new_cache_entry(value)
|
|
47
|
+
|
|
48
|
+
previous&.value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Retrieves the value at the given key. Returns nil if absent or expired.
|
|
53
|
+
# @param key [Object] the cache key.
|
|
54
|
+
# @return [Object, nil] the cached value, or nil.
|
|
55
|
+
def get(key)
|
|
56
|
+
@lock.synchronize do
|
|
57
|
+
entry = @cache[key]
|
|
58
|
+
return nil if entry.nil?
|
|
59
|
+
|
|
60
|
+
if entry.expired?
|
|
61
|
+
@cache.delete(key)
|
|
62
|
+
return nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
entry.value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Removes the value at the given key.
|
|
70
|
+
# @param key [Object] the cache key.
|
|
71
|
+
# @return [Object, nil] the removed value, or nil.
|
|
72
|
+
def remove(key)
|
|
73
|
+
@lock.synchronize do
|
|
74
|
+
entry = @cache.delete(key)
|
|
75
|
+
entry&.value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Removes all entries.
|
|
80
|
+
def clear
|
|
81
|
+
@lock.synchronize do
|
|
82
|
+
@cache.clear
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [Integer] the number of entries (including expired ones).
|
|
87
|
+
def size
|
|
88
|
+
@lock.synchronize do
|
|
89
|
+
@cache.size
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Removes expired entries from the cache.
|
|
94
|
+
def remove_expired_entries
|
|
95
|
+
@lock.synchronize do
|
|
96
|
+
@cache.delete_if { |_key, entry| entry.expired? }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def new_cache_entry(value)
|
|
103
|
+
CacheEntry.new(value, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @ttl)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|