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
|
@@ -38,6 +38,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
38
38
|
# when that endpoint carries a region. When +accessible_regions+ is set, hosts outside those
|
|
39
39
|
# regions are never selected.
|
|
40
40
|
class GdbFailoverPlugin < FailoverPlugin
|
|
41
|
+
TELEMETRY_FAILOVER = 'global database failover'
|
|
42
|
+
|
|
41
43
|
def initialize(service_container, props = ::Concurrent::Map.new)
|
|
42
44
|
super
|
|
43
45
|
|
|
@@ -176,6 +178,10 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
176
178
|
return
|
|
177
179
|
end
|
|
178
180
|
|
|
181
|
+
with_failover_telemetry(TELEMETRY_FAILOVER) { failover_with_plan }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def failover_with_plan
|
|
179
185
|
failover_start = Time.now
|
|
180
186
|
failover_deadline = failover_start + @failover_timeout
|
|
181
187
|
@regionless_hosts.clear
|
|
@@ -189,6 +195,25 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
189
195
|
"failover_timeout_sec=#{@failover_timeout}"
|
|
190
196
|
end
|
|
191
197
|
|
|
198
|
+
writer_candidate, writer_region, mode = failover_plan
|
|
199
|
+
log_failover_plan(writer_candidate, writer_region, mode)
|
|
200
|
+
if mode == GdbFailoverMode::STRICT_WRITER
|
|
201
|
+
count_failover(@writer_failover_counters) { failover_to_writer(writer_candidate, writer_region, failover_deadline) }
|
|
202
|
+
else
|
|
203
|
+
count_failover(@reader_failover_counters) { failover_to_allowed_host(mode, failover_deadline) }
|
|
204
|
+
end
|
|
205
|
+
ensure
|
|
206
|
+
duration_ms = ((Time.now - failover_start) * 1000).round if failover_start
|
|
207
|
+
logger.debug { "Failover duration: #{duration_ms}ms" } if duration_ms
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Finds the writer the topology settled on, and the failover mode that applies to its region.
|
|
211
|
+
#
|
|
212
|
+
# When this fails, failover cannot start, and so it is not known whether it would have been a
|
|
213
|
+
# writer or a reader failover. It is counted as a failed writer failover.
|
|
214
|
+
#
|
|
215
|
+
# @return [Array(Host::HostInfo, String, Symbol)] the writer, its region, and the mode
|
|
216
|
+
def failover_plan
|
|
192
217
|
# This is expected to return once the topology has stabilized, i.e. once the cluster
|
|
193
218
|
# control plane has already chosen a new writer.
|
|
194
219
|
unless host_service.force_refresh_host_list?(verify_writer: true, timeout_sec: @failover_timeout)
|
|
@@ -210,15 +235,11 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
210
235
|
mode = current_failover_mode(writer_region)
|
|
211
236
|
end
|
|
212
237
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
end
|
|
219
|
-
ensure
|
|
220
|
-
duration_ms = ((Time.now - failover_start) * 1000).round if failover_start
|
|
221
|
-
logger.debug { "Failover duration: #{duration_ms}ms" } if duration_ms
|
|
238
|
+
[writer_candidate, writer_region, mode]
|
|
239
|
+
rescue StandardError
|
|
240
|
+
@writer_failover_counters.triggered.inc
|
|
241
|
+
@writer_failover_counters.failed.inc
|
|
242
|
+
raise
|
|
222
243
|
end
|
|
223
244
|
|
|
224
245
|
# Explains which mode was chosen, and what the plugin will do with it.
|
|
@@ -41,6 +41,17 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
41
41
|
PropertyDefinition::IAM_EXPIRATION_SEC.validate!(expiration)
|
|
42
42
|
service_container.storage_service.register(IAM_TOKEN_CACHE_NAME, ttl: expiration)
|
|
43
43
|
@subscribed_methods = SUBSCRIBED_METHODS
|
|
44
|
+
|
|
45
|
+
@telemetry_factory = service_container.telemetry_factory
|
|
46
|
+
@fetch_token_counter = @telemetry_factory.create_counter('iam.fetch_token.count')
|
|
47
|
+
@telemetry_factory.create_gauge('iam.token_cache.size', &self.class.token_cache_size(service_container.storage_service))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The callback of a token cache size gauge. It is built here rather than in an instance, because
|
|
51
|
+
# a gauge is kept for the whole process and a block built in an instance would keep that
|
|
52
|
+
# instance, and the connection it belongs to, alive with it.
|
|
53
|
+
def self.token_cache_size(storage_service)
|
|
54
|
+
-> { storage_service.size(IAM_TOKEN_CACHE_NAME) }
|
|
44
55
|
end
|
|
45
56
|
|
|
46
57
|
def connect(host_info, driver_props, _is_initial_connection, pipeline_callable)
|
|
@@ -128,8 +139,11 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
128
139
|
# and, when there is a cache key, caches it. Without a snapshot the provider is passed as is,
|
|
129
140
|
# and signing fails for lack of credentials.
|
|
130
141
|
def fetch_and_cache_token(region, host, port, user, credentials, cache_key, expiration)
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
@fetch_token_counter.inc
|
|
143
|
+
token = Utils::IamAuthUtils.generate_token(
|
|
144
|
+
region:, hostname: host, port:, user:, credentials_provider: credentials || @credentials_provider,
|
|
145
|
+
telemetry_factory: @telemetry_factory
|
|
146
|
+
)
|
|
133
147
|
if cache_key
|
|
134
148
|
@service_container.storage_service.set(
|
|
135
149
|
IAM_TOKEN_CACHE_NAME, cache_key, Utils::IamAuthUtils.build_token_entry(token, expiration)
|
|
@@ -20,6 +20,7 @@ require_relative '../host/host_role'
|
|
|
20
20
|
require_relative '../host/host_availability'
|
|
21
21
|
require_relative '../logging'
|
|
22
22
|
require_relative '../property_definition'
|
|
23
|
+
require_relative '../services/host_service'
|
|
23
24
|
require_relative '../utils/accessible_regions'
|
|
24
25
|
require_relative '../utils/rds_utils'
|
|
25
26
|
require_relative '../utils/rds_url_type'
|
|
@@ -53,6 +54,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
53
54
|
@retry_interval_sec = PropertyDefinition::INITIAL_CONNECTION_RETRY_INTERVAL_SEC.get_float(props)
|
|
54
55
|
@wait_for_topology_sec = PropertyDefinition::INITIAL_CONNECTION_WAIT_FOR_TOPOLOGY_SEC.get_float(props)
|
|
55
56
|
@host_selector_strategy = PropertyDefinition::INITIAL_CONNECTION_HOST_SELECTOR_STRATEGY.get(props)
|
|
57
|
+
Services::HostService.validate_strategy!(@host_selector_strategy)
|
|
56
58
|
@accessible_regions = Utils::AccessibleRegions.parse(props)
|
|
57
59
|
@subscribed_methods = SUBSCRIBED_METHODS
|
|
58
60
|
|
|
@@ -323,7 +325,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
323
325
|
end
|
|
324
326
|
|
|
325
327
|
host_service.select_host(available_hosts, target_role, @host_selector_strategy)
|
|
326
|
-
rescue StandardError
|
|
328
|
+
rescue StandardError => e
|
|
329
|
+
logger.warn("Failed to select a host using the '#{@host_selector_strategy}' host selection strategy: #{e.message}")
|
|
327
330
|
nil
|
|
328
331
|
end
|
|
329
332
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# See the License for the specific language governing permissions and
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
|
-
require_relative '../../driver_dialects/
|
|
17
|
+
require_relative '../../driver_dialects/mysql2_driver_dialect'
|
|
18
18
|
require_relative '../../driver_dialects/pg_driver_dialect'
|
|
19
19
|
require_relative '../../logging'
|
|
20
20
|
require_relative '../../property_definition'
|
|
@@ -324,7 +324,10 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
324
324
|
def dialect_for_connection(connection)
|
|
325
325
|
name = connection.class.name.to_s
|
|
326
326
|
return DriverDialects::PgDriverDialect.new if name.start_with?('PG::') || name.end_with?('WrapperPgConnection')
|
|
327
|
-
return DriverDialects::
|
|
327
|
+
return DriverDialects::Mysql2DriverDialect.new if name.start_with?('Mysql2::') || name.end_with?('WrapperMysql2Client')
|
|
328
|
+
if name == 'Trilogy' || name.end_with?('WrapperTrilogyClient')
|
|
329
|
+
raise ArgumentError, 'KMS encryption does not support the trilogy driver'
|
|
330
|
+
end
|
|
328
331
|
|
|
329
332
|
raise ArgumentError,
|
|
330
333
|
"Cannot infer the driver dialect from #{name.empty? ? connection.class : name}; " \
|
|
@@ -334,7 +337,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
334
337
|
def dialect_for_driver(driver)
|
|
335
338
|
case driver.to_sym
|
|
336
339
|
when :postgresql, :pg then DriverDialects::PgDriverDialect.new
|
|
337
|
-
when :mysql2, :mysql then DriverDialects::
|
|
340
|
+
when :mysql2, :mysql then DriverDialects::Mysql2DriverDialect.new
|
|
338
341
|
else raise ArgumentError, "Unknown driver #{driver.inspect}; use :postgresql or :mysql2"
|
|
339
342
|
end
|
|
340
343
|
end
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
# See the License for the specific language governing permissions and
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
|
-
require 'base64'
|
|
18
17
|
require 'digest'
|
|
19
18
|
require 'securerandom'
|
|
20
19
|
require_relative '../../logging'
|
|
@@ -105,7 +104,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
105
104
|
|
|
106
105
|
plaintext = with_retry('DECRYPT_DATA_KEY') do
|
|
107
106
|
response = @kms_client.decrypt(
|
|
108
|
-
ciphertext_blob:
|
|
107
|
+
ciphertext_blob: strict_decode64(encrypted_data_key.to_s.delete("\n")),
|
|
109
108
|
**(master_key_arn ? { key_id: master_key_arn } : {})
|
|
110
109
|
)
|
|
111
110
|
response.plaintext.dup.b
|
|
@@ -130,7 +129,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
130
129
|
response = @kms_client.generate_data_key(key_id: master_key_arn, key_spec: KMS_KEY_SPEC)
|
|
131
130
|
GeneratedDataKey.new(
|
|
132
131
|
plaintext: response.plaintext.dup.b,
|
|
133
|
-
encrypted_data_key:
|
|
132
|
+
encrypted_data_key: strict_encode64(response.ciphertext_blob),
|
|
134
133
|
hmac_key: SecureRandom.bytes(HMAC_KEY_LENGTH)
|
|
135
134
|
)
|
|
136
135
|
end
|
|
@@ -272,11 +271,21 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
272
271
|
# @param encrypted_data_key [String]
|
|
273
272
|
# @return [String]
|
|
274
273
|
def data_key_cache_key(encrypted_data_key)
|
|
275
|
-
"#{DATA_KEY_CACHE_PREFIX}#{
|
|
274
|
+
"#{DATA_KEY_CACHE_PREFIX}#{strict_encode64(Digest::SHA256.digest(encrypted_data_key.to_s))}"
|
|
276
275
|
end
|
|
277
276
|
|
|
278
277
|
private
|
|
279
278
|
|
|
279
|
+
# Strict base64 (RFC 4648: no line breaks, padding required), using the encoding built into
|
|
280
|
+
# Array#pack rather than the base64 gem. Decoding raises ArgumentError on invalid input.
|
|
281
|
+
def strict_encode64(bytes)
|
|
282
|
+
[bytes].pack('m0')
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def strict_decode64(encoded)
|
|
286
|
+
encoded.unpack1('m0')
|
|
287
|
+
end
|
|
288
|
+
|
|
280
289
|
def authorize_master_key!(master_key_arn)
|
|
281
290
|
return if @config.master_key_allowed?(master_key_arn)
|
|
282
291
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
17
|
require 'concurrent'
|
|
18
|
+
require_relative '../../driver_dialects/trilogy_driver_dialect'
|
|
18
19
|
require_relative '../../logging'
|
|
19
20
|
require_relative '../../ruby_method'
|
|
20
21
|
require_relative '../../utils/parser/encryption_annotation_parser'
|
|
@@ -205,6 +206,12 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
205
206
|
# @param encryption_utility [Encryption::KmsEncryptionUtility, nil] a utility to use instead
|
|
206
207
|
# of building one
|
|
207
208
|
def initialize(service_container, props = ::Concurrent::Map.new, encryption_utility: nil)
|
|
209
|
+
# Trilogy has no prepared statements, so a parameter could only reach the server written into the
|
|
210
|
+
# SQL, in plaintext, before it could be encrypted.
|
|
211
|
+
if service_container.dialect_service.driver_dialect.is_a?(DriverDialects::TrilogyDriverDialect)
|
|
212
|
+
raise Errors::EncryptionPluginError, 'The kms_encryption plugin does not support the trilogy driver'
|
|
213
|
+
end
|
|
214
|
+
|
|
208
215
|
@service_container = service_container
|
|
209
216
|
@encryption_utility = encryption_utility || Encryption::KmsEncryptionUtility.new(service_container, props)
|
|
210
217
|
# Built up front so that a parser dependency the application has not installed (pg_query, on
|
|
@@ -586,9 +593,10 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
586
593
|
# @raise [Errors::MetadataError] when the statement writes a column the plugin has confirmed is
|
|
587
594
|
# encrypted with a value it cannot encrypt (see {#check_write})
|
|
588
595
|
def parameter_columns(sql)
|
|
589
|
-
|
|
596
|
+
version = executable_comment_version
|
|
597
|
+
annotations = Utils::Parser::EncryptionAnnotationParser.parse_annotations(sql, server_version: version)
|
|
590
598
|
stripped = stripped_sql(sql)
|
|
591
|
-
analysis = analysis_of(stripped)
|
|
599
|
+
analysis = analysis_of(stripped, version)
|
|
592
600
|
write = write_statement?(analysis, stripped)
|
|
593
601
|
inferred = analysis.parameter_column_names
|
|
594
602
|
return {} if !write && annotations.empty? && inferred.empty?
|
|
@@ -607,9 +615,10 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
607
615
|
# that writes nothing, so it is reported as nothing having been read rather than as nothing
|
|
608
616
|
# being there to read.
|
|
609
617
|
#
|
|
618
|
+
# @param version [Integer, nil] see {#executable_comment_version}
|
|
610
619
|
# @return [Utils::Parser::SqlParser::SqlAnalysisResult]
|
|
611
|
-
def analysis_of(sql)
|
|
612
|
-
sql_parser.analyze_sql(sql)
|
|
620
|
+
def analysis_of(sql, version)
|
|
621
|
+
sql_parser.analyze_sql(sql, server_version: version)
|
|
613
622
|
rescue StandardError => e
|
|
614
623
|
logger.warn("The statement could not be analysed: #{e.message}")
|
|
615
624
|
Utils::Parser::SqlParser::SqlAnalysisResult.new(
|
|
@@ -693,8 +702,9 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
693
702
|
def column_configs(sql)
|
|
694
703
|
return {} unless ready_for_statement?(sql)
|
|
695
704
|
|
|
696
|
-
|
|
697
|
-
|
|
705
|
+
version = executable_comment_version
|
|
706
|
+
annotations = Utils::Parser::EncryptionAnnotationParser.parse_annotations(sql, server_version: version)
|
|
707
|
+
tables = statement_tables(analysis_of(stripped_sql(sql), version), annotations)
|
|
698
708
|
return {} if tables.empty?
|
|
699
709
|
|
|
700
710
|
# When two of the statement's tables encrypt a column of the same name, the first of them
|
|
@@ -837,6 +847,19 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
837
847
|
Utils::Parser::EncryptionAnnotationParser.strip_annotations(sql)
|
|
838
848
|
end
|
|
839
849
|
|
|
850
|
+
# The version of the MySQL server the statement is sent to, which decides whether a versioned
|
|
851
|
+
# /*!80023 ... */ comment is part of the statement, and so which column each placeholder after it
|
|
852
|
+
# belongs to (see DriverDialects::DriverDialect#executable_comment_version). It is read from the
|
|
853
|
+
# connection every time rather than kept, since a failover or a blue/green switchover can move
|
|
854
|
+
# the connection to a server of another version.
|
|
855
|
+
#
|
|
856
|
+
# @return [Integer, nil] nil on PostgreSQL, which has no executable comments, and when the
|
|
857
|
+
# connection cannot say
|
|
858
|
+
def executable_comment_version
|
|
859
|
+
@service_container.dialect_service.driver_dialect
|
|
860
|
+
.executable_comment_version(@service_container.connection_service.current_connection)
|
|
861
|
+
end
|
|
862
|
+
|
|
840
863
|
# -- Lazily built collaborators --
|
|
841
864
|
|
|
842
865
|
# Builds the parts of the plugin that need a database connection, the first time a statement
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
DELIMITER $$
|
|
2
|
+
|
|
3
|
+
-- HMAC-SHA256 built from SHA2(), which MySQL has no native HMAC for.
|
|
4
|
+
DROP FUNCTION IF EXISTS hmac_sha256$$
|
|
5
|
+
CREATE FUNCTION hmac_sha256(
|
|
6
|
+
message VARBINARY(65535),
|
|
7
|
+
key_data VARBINARY(64)
|
|
8
|
+
)
|
|
9
|
+
RETURNS VARBINARY(32)
|
|
10
|
+
DETERMINISTIC
|
|
11
|
+
NO SQL
|
|
12
|
+
BEGIN
|
|
13
|
+
DECLARE block_size INT DEFAULT 64; -- SHA-256 block size
|
|
14
|
+
DECLARE key_padded VARBINARY(64);
|
|
15
|
+
DECLARE ipad VARBINARY(64);
|
|
16
|
+
DECLARE opad VARBINARY(64);
|
|
17
|
+
DECLARE i INT DEFAULT 1;
|
|
18
|
+
DECLARE key_byte INT;
|
|
19
|
+
|
|
20
|
+
-- Reduce an over-long key to its hash, then zero-pad the key to the block size.
|
|
21
|
+
IF LENGTH(key_data) > block_size THEN
|
|
22
|
+
SET key_padded = CONCAT(UNHEX(SHA2(key_data, 256)), REPEAT(X'00', block_size - 32));
|
|
23
|
+
ELSE
|
|
24
|
+
SET key_padded = CONCAT(key_data, REPEAT(X'00', block_size - LENGTH(key_data)));
|
|
25
|
+
END IF;
|
|
26
|
+
|
|
27
|
+
-- ipad = key XOR 0x36 per byte, opad = key XOR 0x5C per byte, one raw byte at a time.
|
|
28
|
+
SET ipad = X'';
|
|
29
|
+
SET opad = X'';
|
|
30
|
+
WHILE i <= block_size DO
|
|
31
|
+
SET key_byte = ASCII(SUBSTRING(key_padded, i, 1));
|
|
32
|
+
SET ipad = CONCAT(ipad, UNHEX(LPAD(HEX(key_byte ^ 0x36), 2, '0')));
|
|
33
|
+
SET opad = CONCAT(opad, UNHEX(LPAD(HEX(key_byte ^ 0x5C), 2, '0')));
|
|
34
|
+
SET i = i + 1;
|
|
35
|
+
END WHILE;
|
|
36
|
+
|
|
37
|
+
RETURN UNHEX(SHA2(CONCAT(opad, UNHEX(SHA2(CONCAT(ipad, message), 256))), 256));
|
|
38
|
+
END$$
|
|
39
|
+
|
|
40
|
+
-- Verifies that a stored value carries a valid HMAC for the given HMAC key.
|
|
41
|
+
DROP FUNCTION IF EXISTS verify_encrypted_data_hmac$$
|
|
42
|
+
CREATE FUNCTION verify_encrypted_data_hmac(
|
|
43
|
+
data VARBINARY(65535),
|
|
44
|
+
hmac_key VARBINARY(32)
|
|
45
|
+
)
|
|
46
|
+
RETURNS BOOLEAN
|
|
47
|
+
DETERMINISTIC
|
|
48
|
+
NO SQL
|
|
49
|
+
BEGIN
|
|
50
|
+
-- Minimum payload: 32 (HMAC) + 4 (key id) + 1 (type) + 12 (IV) + 0 (ciphertext) + 16 (GCM tag) = 65.
|
|
51
|
+
IF data IS NULL OR LENGTH(data) < 65 THEN
|
|
52
|
+
RETURN FALSE;
|
|
53
|
+
END IF;
|
|
54
|
+
|
|
55
|
+
RETURN SUBSTRING(data, 1, 32) = hmac_sha256(SUBSTRING(data, 33), hmac_key);
|
|
56
|
+
END$$
|
|
57
|
+
|
|
58
|
+
-- Called from a BEFORE INSERT trigger (and by the BEFORE UPDATE procedure) to reject a value that is
|
|
59
|
+
-- not a valid kms_encryption payload. Replace SCHEMA_NAME with encryption_metadata_schema.
|
|
60
|
+
-- The COMMENT records which version of this SQL is installed. Check it with:
|
|
61
|
+
-- SELECT ROUTINE_COMMENT FROM information_schema.ROUTINES
|
|
62
|
+
-- WHERE ROUTINE_SCHEMA = DATABASE() AND ROUTINE_NAME = 'validate_encrypted_data_hmac_before_insert';
|
|
63
|
+
DROP PROCEDURE IF EXISTS validate_encrypted_data_hmac_before_insert$$
|
|
64
|
+
CREATE PROCEDURE validate_encrypted_data_hmac_before_insert(
|
|
65
|
+
IN p_table_name VARCHAR(64),
|
|
66
|
+
IN p_column_name VARCHAR(64),
|
|
67
|
+
IN column_value VARBINARY(65535)
|
|
68
|
+
)
|
|
69
|
+
COMMENT 'aws-advanced-ruby-driver-wrapper kms_encryption trigger v1'
|
|
70
|
+
BEGIN
|
|
71
|
+
DECLARE v_hmac_key VARBINARY(32);
|
|
72
|
+
|
|
73
|
+
IF column_value IS NOT NULL THEN
|
|
74
|
+
SELECT ks.hmac_key INTO v_hmac_key
|
|
75
|
+
FROM SCHEMA_NAME.encryption_metadata em
|
|
76
|
+
JOIN SCHEMA_NAME.key_storage ks ON em.key_id = ks.id
|
|
77
|
+
WHERE em.table_name = p_table_name
|
|
78
|
+
AND em.column_name = p_column_name
|
|
79
|
+
LIMIT 1;
|
|
80
|
+
|
|
81
|
+
IF v_hmac_key IS NULL THEN
|
|
82
|
+
SIGNAL SQLSTATE '45000'
|
|
83
|
+
SET MESSAGE_TEXT = 'No HMAC key is configured for the encrypted column';
|
|
84
|
+
END IF;
|
|
85
|
+
|
|
86
|
+
IF NOT verify_encrypted_data_hmac(column_value, v_hmac_key) THEN
|
|
87
|
+
SIGNAL SQLSTATE '45000'
|
|
88
|
+
SET MESSAGE_TEXT = 'Column does not carry a valid HMAC tag (plaintext or tampered value)';
|
|
89
|
+
END IF;
|
|
90
|
+
END IF;
|
|
91
|
+
END$$
|
|
92
|
+
|
|
93
|
+
-- Called from a BEFORE UPDATE trigger. An UPDATE that leaves the value as it was is not
|
|
94
|
+
-- re-checked; see "Updates after a key rotation".
|
|
95
|
+
DROP PROCEDURE IF EXISTS validate_encrypted_data_hmac_before_update$$
|
|
96
|
+
CREATE PROCEDURE validate_encrypted_data_hmac_before_update(
|
|
97
|
+
IN p_table_name VARCHAR(64),
|
|
98
|
+
IN p_column_name VARCHAR(64),
|
|
99
|
+
IN new_value VARBINARY(65535),
|
|
100
|
+
IN old_value VARBINARY(65535)
|
|
101
|
+
)
|
|
102
|
+
BEGIN
|
|
103
|
+
IF NOT (new_value <=> old_value) THEN
|
|
104
|
+
CALL validate_encrypted_data_hmac_before_insert(p_table_name, p_column_name, new_value);
|
|
105
|
+
END IF;
|
|
106
|
+
END$$
|
|
107
|
+
|
|
108
|
+
DELIMITER ;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- pgcrypto provides hmac(). If it is already installed in a schema other than public,
|
|
2
|
+
-- replace public in public.hmac below with that schema.
|
|
3
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
|
|
4
|
+
|
|
5
|
+
-- Replace 'encrypt' below if encryption_metadata_schema is set to something else.
|
|
6
|
+
-- A trigger runs with the search_path of the session that fires it. Pinning it to pg_catalog
|
|
7
|
+
-- stops a session from putting its own functions or operators, such as hmac() or <>, ahead of
|
|
8
|
+
-- the built-in ones to get a plaintext value past this check.
|
|
9
|
+
CREATE OR REPLACE FUNCTION enforce_encrypted_column() RETURNS trigger
|
|
10
|
+
SET search_path = pg_catalog, pg_temp
|
|
11
|
+
AS $$
|
|
12
|
+
DECLARE
|
|
13
|
+
col_name text := TG_ARGV[0];
|
|
14
|
+
col_value bytea;
|
|
15
|
+
hmac_key bytea;
|
|
16
|
+
unchanged boolean;
|
|
17
|
+
BEGIN
|
|
18
|
+
-- An UPDATE that leaves the encrypted value as it was is not re-checked; see
|
|
19
|
+
-- "Updates after a key rotation".
|
|
20
|
+
IF TG_OP = 'UPDATE' THEN
|
|
21
|
+
EXECUTE format('SELECT ($1).%I IS NOT DISTINCT FROM ($2).%I', col_name, col_name)
|
|
22
|
+
INTO unchanged USING NEW, OLD;
|
|
23
|
+
IF unchanged THEN
|
|
24
|
+
RETURN NEW;
|
|
25
|
+
END IF;
|
|
26
|
+
END IF;
|
|
27
|
+
|
|
28
|
+
EXECUTE format('SELECT ($1).%I', col_name) INTO col_value USING NEW;
|
|
29
|
+
IF col_value IS NULL THEN
|
|
30
|
+
RETURN NEW;
|
|
31
|
+
END IF;
|
|
32
|
+
|
|
33
|
+
-- Read the column's current HMAC key from key_storage.
|
|
34
|
+
SELECT ks.hmac_key INTO hmac_key
|
|
35
|
+
FROM encrypt.encryption_metadata em
|
|
36
|
+
JOIN encrypt.key_storage ks ON em.key_id = ks.id
|
|
37
|
+
WHERE em.table_name = TG_TABLE_NAME AND em.column_name = col_name;
|
|
38
|
+
|
|
39
|
+
IF hmac_key IS NULL THEN
|
|
40
|
+
RAISE EXCEPTION 'No HMAC key is configured for %.%', TG_TABLE_NAME, col_name;
|
|
41
|
+
END IF;
|
|
42
|
+
|
|
43
|
+
IF length(col_value) < 65
|
|
44
|
+
OR substring(col_value from 1 for 32) <> public.hmac(substring(col_value from 33), hmac_key, 'sha256'::text) THEN
|
|
45
|
+
RAISE EXCEPTION 'Column %.% does not carry a valid HMAC tag (plaintext or tampered value)', TG_TABLE_NAME, col_name;
|
|
46
|
+
END IF;
|
|
47
|
+
|
|
48
|
+
RETURN NEW;
|
|
49
|
+
END;
|
|
50
|
+
$$ LANGUAGE plpgsql;
|
|
51
|
+
|
|
52
|
+
-- Records which version of this SQL is installed. Check it with:
|
|
53
|
+
-- SELECT obj_description('enforce_encrypted_column'::regproc, 'pg_proc');
|
|
54
|
+
COMMENT ON FUNCTION enforce_encrypted_column() IS 'aws-advanced-ruby-driver-wrapper kms_encryption trigger v2';
|
|
@@ -19,6 +19,7 @@ require_relative '../errors'
|
|
|
19
19
|
require_relative '../logging'
|
|
20
20
|
require_relative '../property_definition'
|
|
21
21
|
require_relative '../utils/aws_credentials_utils'
|
|
22
|
+
require_relative '../utils/telemetry/telemetry_const'
|
|
22
23
|
|
|
23
24
|
module AwsAdvancedRubyDriverWrapper
|
|
24
25
|
module Plugins
|
|
@@ -27,6 +28,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
27
28
|
|
|
28
29
|
SUBSCRIBED_METHODS = Set['connect', 'internal_connect'].freeze
|
|
29
30
|
SECRETS_MANAGER_CACHE_NAME = :secrets_manager
|
|
31
|
+
TELEMETRY_FETCH_CREDENTIALS = 'fetch credentials'
|
|
30
32
|
MIN_EXPIRATION_SEC = 300
|
|
31
33
|
SYNC_FETCH_TIMEOUT_SEC = 60
|
|
32
34
|
# The partition segment of an ARN varies across AWS partitions, so match any of them.
|
|
@@ -79,6 +81,12 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
79
81
|
storage_service.clear(SECRETS_MANAGER_CACHE_NAME)
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
# Forgets fetches inherited by a forked child. A fetch running at fork time lost its thread,
|
|
85
|
+
# so its future never resolves and would otherwise be returned to every later fetch for its key.
|
|
86
|
+
def release_pending_refreshes_after_fork
|
|
87
|
+
@pending_refreshes.clear
|
|
88
|
+
end
|
|
89
|
+
|
|
82
90
|
# The secret's value depends on which secret is read, from which region and endpoint, and
|
|
83
91
|
# with which AWS credentials, so all of them are part of the key. Connections that differ in
|
|
84
92
|
# any of them neither share a cached secret nor wait on each other's fetch.
|
|
@@ -120,6 +128,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
120
128
|
)
|
|
121
129
|
|
|
122
130
|
@subscribed_methods = SUBSCRIBED_METHODS
|
|
131
|
+
@telemetry_factory = service_container.telemetry_factory
|
|
132
|
+
@fetch_credentials_counter = @telemetry_factory.create_counter('secrets_manager.fetch_credentials.count')
|
|
123
133
|
end
|
|
124
134
|
|
|
125
135
|
def connect(_host_info, driver_props, _is_initial_connection, pipeline_callable)
|
|
@@ -205,7 +215,17 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
205
215
|
end
|
|
206
216
|
|
|
207
217
|
# Returns true if credentials were freshly fetched from the service (suppresses login-error retry).
|
|
218
|
+
#
|
|
219
|
+
# Each lookup is counted and recorded as a span, whether it is answered from the cache or from
|
|
220
|
+
# the service.
|
|
208
221
|
def fetch_secret_and_report_if_fresh?(force:)
|
|
222
|
+
@fetch_credentials_counter.inc
|
|
223
|
+
@telemetry_factory.in_context(TELEMETRY_FETCH_CREDENTIALS, Utils::Telemetry::TraceLevel::NESTED, record_failure: true) do
|
|
224
|
+
load_secret_and_report_if_fresh?(force:)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def load_secret_and_report_if_fresh?(force:)
|
|
209
229
|
credentials = Utils::AwsCredentialsUtils.snapshot(@credentials_provider)
|
|
210
230
|
cache_key = secret_cache_key(credentials)
|
|
211
231
|
entry = cache_key && @service_container.storage_service.get(SECRETS_MANAGER_CACHE_NAME, cache_key)
|
|
@@ -244,7 +264,9 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
244
264
|
else
|
|
245
265
|
Concurrent::Promises.future_on(:io) { fetch_and_store_secret(nil, credentials) }
|
|
246
266
|
end
|
|
247
|
-
|
|
267
|
+
# value! returns nil instead of raising when the timeout elapses; a completed fetch always
|
|
268
|
+
# returns an entry or raises.
|
|
269
|
+
future.value!(SYNC_FETCH_TIMEOUT_SEC) || raise(Timeout::Error)
|
|
248
270
|
rescue Concurrent::CancelledOperationError, Timeout::Error
|
|
249
271
|
raise Errors::SecretsManagerAuthError,
|
|
250
272
|
"Timed out fetching secret after #{SYNC_FETCH_TIMEOUT_SEC}s"
|
|
@@ -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
|
|
|
@@ -45,17 +46,20 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
45
46
|
def initialize(*, **)
|
|
46
47
|
ensure_pg!
|
|
47
48
|
config = Utils::ConnectionConfigParser.parse(:postgresql, *, **)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
telemetry_factory = Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
|
|
50
|
+
telemetry_factory.in_context("#{self.class.name}.new", Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do
|
|
51
|
+
@service_container = Services::ServiceUtility.create_standard_container(config, telemetry_factory:)
|
|
52
|
+
@service_container.host_service.refresh_host_list
|
|
53
|
+
@prepared_on = {}
|
|
54
|
+
@prepared_sql = {}
|
|
55
|
+
@async_conn = nil
|
|
56
|
+
@async_sql = nil
|
|
57
|
+
@copy_conn = nil
|
|
58
|
+
@copy_sql = nil
|
|
59
|
+
@lo_conn = nil
|
|
60
|
+
conn_service = @service_container.connection_service
|
|
61
|
+
@service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
|
|
62
|
+
end
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
# Every canonical pg operation that talks to the server, and everything this class has to know about one: the
|
|
@@ -173,6 +177,10 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
173
177
|
lotell: :lo_tell, lotruncate: :lo_truncate
|
|
174
178
|
}.freeze
|
|
175
179
|
|
|
180
|
+
# The operations pg yields the result of when given a block, clearing it once the block returns and
|
|
181
|
+
# returning the block's value in its place.
|
|
182
|
+
YIELDS_RESULT = %i[exec async_exec exec_params exec_prepared].freeze
|
|
183
|
+
|
|
176
184
|
# A statement can also be prepared by sending a +PREPARE+ rather than by calling pg's own
|
|
177
185
|
# +prepare+, and the +exec_prepared+ that runs it looks no different either way. The two are read
|
|
178
186
|
# here so that a statement prepared the first way is remembered like one prepared the second, and
|
|
@@ -194,24 +202,24 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
194
202
|
|
|
195
203
|
# Explicitly define critical methods (bypass method_missing to avoid method_missing overhead).
|
|
196
204
|
|
|
197
|
-
def exec(sql, *params)
|
|
198
|
-
execute_operation(:exec, [sql, *params])
|
|
205
|
+
def exec(sql, *params, &)
|
|
206
|
+
execute_operation(:exec, [sql, *params], &)
|
|
199
207
|
end
|
|
200
208
|
|
|
201
209
|
# pg spells this operation +exec+, +query+, +async_exec+ and +async_query+, all of which run the
|
|
202
210
|
# same libpq call. +query+ is defined here rather than left to method_missing because it is the
|
|
203
211
|
# spelling applications use most after +exec+, and it enters the pipeline as +connection.exec+,
|
|
204
212
|
# since that is the libpq operation being performed.
|
|
205
|
-
def query(sql, *params)
|
|
206
|
-
execute_operation(:exec, [sql, *params], spelling: :query)
|
|
213
|
+
def query(sql, *params, &)
|
|
214
|
+
execute_operation(:exec, [sql, *params], spelling: :query, &)
|
|
207
215
|
end
|
|
208
216
|
|
|
209
|
-
def exec_params(sql, params, result_format = 0, type_map = nil)
|
|
210
|
-
execute_operation(:exec_params, [sql, params, result_format, type_map])
|
|
217
|
+
def exec_params(sql, params, result_format = 0, type_map = nil, &)
|
|
218
|
+
execute_operation(:exec_params, [sql, params, result_format, type_map], &)
|
|
211
219
|
end
|
|
212
220
|
|
|
213
|
-
def async_exec(sql, *params)
|
|
214
|
-
execute_operation(:async_exec, [sql, *params])
|
|
221
|
+
def async_exec(sql, *params, &)
|
|
222
|
+
execute_operation(:async_exec, [sql, *params], &)
|
|
215
223
|
end
|
|
216
224
|
|
|
217
225
|
def transaction(&)
|
|
@@ -224,6 +232,14 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
224
232
|
|
|
225
233
|
alias finish close
|
|
226
234
|
|
|
235
|
+
# Stops the background topology monitor of this connection's cluster, which closes the connections it
|
|
236
|
+
# holds to the database it was started for. The next connection that needs topology starts a new one.
|
|
237
|
+
# PostgreSQL refuses to drop a database that other sessions are connected to, so this has to happen
|
|
238
|
+
# before dropping the database the monitor was started for.
|
|
239
|
+
def stop_topology_monitor
|
|
240
|
+
@service_container.host_service.host_list_provider&.stop_monitor
|
|
241
|
+
end
|
|
242
|
+
|
|
227
243
|
# Resets the connection through the pipeline and returns this wrapper, so the reset connection stays
|
|
228
244
|
# usable through it. The driver's own reset tears down and re-establishes the underlying socket, which
|
|
229
245
|
# clears any server-side session state, so the tracked session state is reset to match.
|
|
@@ -242,8 +258,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
242
258
|
execute_operation(:send_prepare, [stmt_name, sql, param_types])
|
|
243
259
|
end
|
|
244
260
|
|
|
245
|
-
def exec_prepared(stmt_name, params = [], result_format = 0, type_map = nil)
|
|
246
|
-
execute_operation(:exec_prepared, [stmt_name, params, result_format, type_map])
|
|
261
|
+
def exec_prepared(stmt_name, params = [], result_format = 0, type_map = nil, &)
|
|
262
|
+
execute_operation(:exec_prepared, [stmt_name, params, result_format, type_map], &)
|
|
247
263
|
end
|
|
248
264
|
|
|
249
265
|
def describe_prepared(stmt_name)
|
|
@@ -304,7 +320,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
304
320
|
|
|
305
321
|
def method_missing(method_name, *args, **kwargs, &)
|
|
306
322
|
conn = current_conn
|
|
307
|
-
raise
|
|
323
|
+
raise PG::ConnectionBad, 'connection is not initialized' if conn.nil?
|
|
308
324
|
raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
|
|
309
325
|
|
|
310
326
|
operation = operation_for(method_name)
|
|
@@ -336,10 +352,14 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
336
352
|
# made, since an `after` step may be what forgets it, and it is handed to the result as well, so that a
|
|
337
353
|
# plugin which has to inspect the statement still sees it when the rows are read.
|
|
338
354
|
def execute_operation(operation, args = [], kwargs = {}, spelling: operation, &)
|
|
355
|
+
if block_given? && YIELDS_RESULT.include?(operation)
|
|
356
|
+
return yield_result(execute_operation(operation, args, kwargs, spelling: spelling), &)
|
|
357
|
+
end
|
|
358
|
+
|
|
339
359
|
spec = OPERATIONS[operation] || { method: "connection.#{operation}" }
|
|
340
360
|
conn = current_conn
|
|
341
361
|
# Guard against a missing connection. Fail loudly instead.
|
|
342
|
-
raise
|
|
362
|
+
raise PG::ConnectionBad, 'connection is not initialized' if conn.nil?
|
|
343
363
|
|
|
344
364
|
# Only forward keyword arguments when there are any.
|
|
345
365
|
sql = sql_for(spec, args)
|
|
@@ -510,6 +530,12 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
510
530
|
@network_bound_methods ||= driver_dialect.network_bound_methods
|
|
511
531
|
end
|
|
512
532
|
|
|
533
|
+
def yield_result(result)
|
|
534
|
+
yield result
|
|
535
|
+
ensure
|
|
536
|
+
result.clear
|
|
537
|
+
end
|
|
538
|
+
|
|
513
539
|
def wrap_pg_result(result, sql = nil)
|
|
514
540
|
return result unless result.is_a?(PG::Result)
|
|
515
541
|
|