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.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. metadata +227 -0
@@ -0,0 +1,72 @@
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 'cluster_topology_monitor'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Monitoring
21
+ # Topology monitor for Aurora Global Databases spanning multiple AWS regions.
22
+ # Extends ClusterTopologyMonitor by using region-aware instance templates and
23
+ # a global topology query that returns region information per instance.
24
+ class GlobalClusterTopologyMonitor < ClusterTopologyMonitor
25
+ # @param service_container [Services::ServiceContainer]
26
+ # @param cluster_id [String]
27
+ # @param instance_template [Host::HostInfo] default template (used for initial connection)
28
+ # @param instance_templates_by_region [Hash{String => Host::HostInfo}] region -> instance template
29
+ # @param topology_utils [Utils::GlobalAuroraTopologyUtils]
30
+ # @param monitoring_driver_props [Hash]
31
+ # @param monitoring_wrapper_props [Hash]
32
+ def initialize(
33
+ service_container:,
34
+ cluster_id:,
35
+ instance_template:,
36
+ instance_templates_by_region:,
37
+ topology_utils:,
38
+ monitoring_driver_props:,
39
+ monitoring_wrapper_props: {}
40
+ )
41
+ super(
42
+ service_container:,
43
+ cluster_id:,
44
+ instance_template:,
45
+ topology_utils:,
46
+ monitoring_driver_props:,
47
+ monitoring_wrapper_props:
48
+ )
49
+ @instance_templates_by_region = instance_templates_by_region
50
+ end
51
+
52
+ private
53
+
54
+ def query_topology(conn)
55
+ @topology_utils.query_global_topology(conn, initial_host_info, @instance_templates_by_region)
56
+ end
57
+
58
+ def resolve_instance_template(instance_id, conn)
59
+ region = @topology_utils.query_region(instance_id, conn)
60
+ return @instance_template if region.nil?
61
+
62
+ template = @instance_templates_by_region[region]
63
+ if template.nil?
64
+ logger.warn("[#{@cluster_id}] No instance template for region '#{region}'")
65
+ return @instance_template
66
+ end
67
+
68
+ template
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,99 @@
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 'monitor_state'
18
+ require_relative '../logging'
19
+ require 'concurrent'
20
+
21
+ module AwsAdvancedRubyDriverWrapper
22
+ module Monitoring
23
+ # Base class for all monitors. Manages thread lifecycle, state transitions,
24
+ # and activity tracking. Subclasses implement #monitor with their polling logic.
25
+ class Monitor
26
+ include Logging
27
+
28
+ attr_reader :last_activity_sec
29
+
30
+ def state
31
+ @state.value
32
+ end
33
+
34
+ def initialize(termination_timeout_sec: 30.0)
35
+ @termination_timeout_sec = termination_timeout_sec
36
+ @state = Concurrent::AtomicReference.new(nil)
37
+ @stop_flag = Concurrent::AtomicBoolean.new(false)
38
+ update_activity
39
+ @thread = nil
40
+ @lock = Mutex.new
41
+ end
42
+
43
+ def start
44
+ @lock.synchronize do
45
+ return if @state.value == MonitorState::RUNNING
46
+
47
+ @stop_flag.make_false
48
+ @state.set(MonitorState::RUNNING)
49
+ @thread = Thread.new { run }
50
+ @thread.name = "monitor-#{monitor_thread_suffix}"
51
+ logger.debug("Started monitoring thread: #{@thread.name}")
52
+ end
53
+ end
54
+
55
+ def stop
56
+ @stop_flag.make_true
57
+
58
+ thread = @thread
59
+ if thread&.alive? && thread != Thread.current
60
+ thread.join(@termination_timeout_sec)
61
+ thread.kill if thread.alive?
62
+ end
63
+ @thread = nil
64
+
65
+ @state.set(MonitorState::STOPPED)
66
+ close
67
+ logger.debug("Stopped monitoring thread: monitor-#{monitor_thread_suffix}")
68
+ end
69
+
70
+ def stopped?
71
+ @stop_flag.true?
72
+ end
73
+
74
+ def close; end
75
+
76
+ private
77
+
78
+ def run
79
+ monitor
80
+ rescue StandardError => e
81
+ logger.error("Exception in monitoring thread monitor-#{monitor_thread_suffix}: #{e.message}")
82
+ @state.set(MonitorState::ERROR)
83
+ ensure
84
+ @state.compare_and_set(MonitorState::RUNNING, MonitorState::STOPPED)
85
+ end
86
+
87
+ def monitor_thread_suffix
88
+ class_name = self.class.name
89
+ return 'unknown' unless class_name
90
+
91
+ class_name.split('::').last.downcase
92
+ end
93
+
94
+ def update_activity
95
+ @last_activity_sec = Process.clock_gettime(Process::CLOCK_MONOTONIC)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,57 @@
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 Monitoring
19
+ # Thread-safe connection wrapper with compare-and-set semantics.
20
+ # Automatically closes the old connection when replaced.
21
+ class MonitorConnection
22
+ # @param driver_dialect [DriverDialects::DriverDialect] the driver dialect used to safely close connections.
23
+ def initialize(driver_dialect)
24
+ @connection = Concurrent::AtomicReference.new(nil)
25
+ @driver_dialect = driver_dialect
26
+ end
27
+
28
+ # Returns the current connection, or nil.
29
+ def get
30
+ @connection.value
31
+ end
32
+
33
+ # Replaces the current connection. Closes the old one unless close_old is false.
34
+ # @param new_conn [Object, nil] the new connection.
35
+ # @param close_old [Boolean] whether to close the previous connection.
36
+ def set(new_conn, close_old: true)
37
+ old = @connection.get_and_set(new_conn)
38
+ return unless close_old && old && !old.equal?(new_conn)
39
+
40
+ @driver_dialect.close_connection(old)
41
+ end
42
+
43
+ # Atomically sets the connection only if the current value is `expected` (identity check).
44
+ # @param expected [Object, nil] the expected current connection.
45
+ # @param new_conn [Object] the new connection to set.
46
+ # @return [Boolean] true if the swap succeeded.
47
+ def compare_and_set(expected, new_conn)
48
+ @connection.compare_and_set(expected, new_conn)
49
+ end
50
+
51
+ # Closes and nils the connection.
52
+ def close
53
+ set(nil)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
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 Monitoring
19
+ module MonitorState
20
+ RUNNING = :running
21
+ STOPPED = :stopped
22
+ ERROR = :error
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,429 @@
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
+ # Loads the gem's top-level setup (version constant, global configuration) that this client relies on,
18
+ # so requiring this file alone is enough to use the MySQL client.
19
+ require_relative '../aws_advanced_ruby_driver_wrapper'
20
+ require_relative 'utils/connection_config_parser'
21
+ require_relative 'utils/sql_encoding'
22
+ require_relative 'services/service_utility'
23
+ require_relative 'ruby_method'
24
+ require_relative 'errors'
25
+
26
+ module AwsAdvancedRubyDriverWrapper
27
+ class WrapperMysql2Client
28
+ def self.new(**)
29
+ instance = allocate
30
+ instance.send(:initialize, **)
31
+ instance
32
+ end
33
+
34
+ def initialize(**)
35
+ ensure_mysql2!
36
+ config = Utils::ConnectionConfigParser.parse(:mysql2, **)
37
+ @service_container = Services::ServiceUtility.create_standard_container(config)
38
+ @service_container.host_service.refresh_host_list
39
+ @async_conn = nil
40
+ @async_sql = nil
41
+ @last_sql = nil
42
+ conn_service = @service_container.connection_service
43
+ @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
44
+ end
45
+
46
+ # Explicitly define critical methods (bypass method_missing to avoid method_missing overhead)
47
+
48
+ # This is how mysql2 sends a statement asynchronously as well: +query(sql, async: true)+ returns
49
+ # nothing and the result is read afterward by +async_result+. The connection the statement was
50
+ # sent on is remembered for that read, and so is its SQL, since the read is a call of its own and
51
+ # carries neither.
52
+ #
53
+ # What is remembered is the copy of the SQL that plugins inspect, made once here rather than on
54
+ # every call that is handed it later. The driver is sent the SQL as the application wrote it.
55
+ def query(sql, options = {})
56
+ inspected = Utils::SqlEncoding.inspectable(sql)
57
+ result = pm.execute(RubyMethod::CONNECTION_QUERY, current_conn, ->(*a) { current_conn.query(*a) }, sql, options,
58
+ sql: inspected)
59
+ @last_sql = inspected
60
+ if options[:async]
61
+ @async_conn = current_conn
62
+ @async_sql = inspected
63
+ end
64
+ wrap_mysql_result(result, inspected)
65
+ end
66
+
67
+ def prepare(sql)
68
+ inspected = Utils::SqlEncoding.inspectable(sql)
69
+ mysql_stmt = pm.execute(RubyMethod::CONNECTION_PREPARE, current_conn, ->(*a) { current_conn.prepare(*a) }, sql,
70
+ sql: inspected)
71
+ Mysql2WrapperStatement.new(@service_container, current_conn, mysql_stmt, inspected)
72
+ end
73
+
74
+ def escape(string)
75
+ pm.execute(RubyMethod::CONNECTION_ESCAPE, current_conn, ->(*a) { current_conn.escape(*a) }, string)
76
+ end
77
+
78
+ def ping
79
+ pm.execute(RubyMethod::CONNECTION_PING, current_conn, -> { current_conn.ping })
80
+ end
81
+
82
+ def close
83
+ pm.execute(RubyMethod::CONNECTION_CLOSE, current_conn, -> { current_conn.close })
84
+ end
85
+
86
+ # -- Async readers (check bounded to @async_conn) --
87
+
88
+ # The result of a statement that was sent with +async: true+. It is read by a call of its own, so
89
+ # it is handed the SQL of the statement it belongs to, and it is refused on any connection other
90
+ # than the one that statement was sent on.
91
+ def async_result
92
+ result = pm.execute(RubyMethod::CONNECTION_ASYNC_RESULT, current_conn, -> { current_conn.async_result },
93
+ bounded_conn: @async_conn, sql: @async_sql)
94
+ sql = @async_sql
95
+ @async_conn = nil
96
+ @async_sql = nil
97
+ wrap_mysql_result(result, sql)
98
+ end
99
+
100
+ def store_result
101
+ result = pm.execute(RubyMethod::CONNECTION_STORE_RESULT, current_conn, -> { current_conn.store_result },
102
+ bounded_conn: @async_conn, sql: @async_sql)
103
+ sql = @async_sql
104
+ @async_conn = nil
105
+ @async_sql = nil
106
+ wrap_mysql_result(result, sql)
107
+ end
108
+
109
+ def more_results?
110
+ pm.execute(RubyMethod::CONNECTION_MORE_RESULTS, current_conn, -> { current_conn.more_results? }, bounded_conn: @async_conn)
111
+ end
112
+
113
+ # A statement that leaves more than one result set is read by moving to each in turn and storing
114
+ # it. Storing a result forgets the connection the statement was sent on and the SQL that was sent
115
+ # on it, and every one of those results belongs to that statement, so both are put back for the
116
+ # read that follows and dropped once there is nothing left to read.
117
+ def next_result
118
+ result = pm.execute(RubyMethod::CONNECTION_NEXT_RESULT, current_conn, -> { current_conn.next_result },
119
+ bounded_conn: @async_conn)
120
+ if result
121
+ @async_conn = current_conn
122
+ @async_sql = @last_sql
123
+ else
124
+ @async_conn = nil
125
+ @async_sql = nil
126
+ end
127
+ result
128
+ end
129
+
130
+ # -- method_missing: covers non-network calls and rarely used network calls --
131
+
132
+ # The network calls that are rare enough not to be worth a method of their own. They are entered
133
+ # into the pipeline under the name the pipeline knows them by rather than as a bare string, so
134
+ # that the connection each is bound to is checked. mysql2 gives none of its calls a second
135
+ # spelling, so unlike pg there is nothing here to translate.
136
+ DYNAMIC_METHODS = {
137
+ abandon_results!: RubyMethod::CONNECTION_ABANDON_RESULTS,
138
+ select_db: RubyMethod::CONNECTION_SELECT_DB,
139
+ set_server_option: RubyMethod::CONNECTION_SET_SERVER_OPTION
140
+ }.freeze
141
+
142
+ # Draining what is left of a statement can only be done on the connection it was sent on.
143
+ BOUNDED_TO_ASYNC = Set[:abandon_results!].freeze
144
+
145
+ def method_missing(method_name, *args, **kwargs, &)
146
+ conn = current_conn
147
+ raise NoMethodError, 'Connection not initialized' if conn.nil?
148
+ raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
149
+
150
+ method_key = "connection.#{method_name}"
151
+ return conn.send(method_name, *args, **kwargs, &) unless network_bound_methods.include?(method_key)
152
+
153
+ execute_dynamic(method_name, method_key, args, kwargs, &)
154
+ end
155
+
156
+ def respond_to_missing?(method, include_private = false)
157
+ current_conn.respond_to?(method, include_private) || super
158
+ end
159
+
160
+ # A concise representation that never exposes the connection config (which carries
161
+ # credentials) or the cached SQL text this instance holds. Defined so a default
162
+ # dump - via logging, interpolation, `pp`, or a backtrace - cannot leak either.
163
+ def inspect
164
+ format('#<%<class>s:0x%<addr>016x>', class: self.class.name, addr: object_id << 1)
165
+ end
166
+ alias to_s inspect
167
+
168
+ def pretty_print(pp)
169
+ pp.text(inspect)
170
+ end
171
+
172
+ private
173
+
174
+ # Runs a call that reached method_missing through the pipeline, telling the plugins the connection
175
+ # it is bound to, which is known here rather than from the arguments of the call.
176
+ def execute_dynamic(method_name, method_key, args, kwargs, &)
177
+ bounded_conn = BOUNDED_TO_ASYNC.include?(method_name) ? @async_conn : nil
178
+ result = pm.execute(
179
+ DYNAMIC_METHODS[method_name] || method_key, current_conn,
180
+ ->(*a, **opts, &b) { current_conn.send(method_name, *a, **opts, &b) },
181
+ *args, **kwargs, bounded_conn: bounded_conn, &
182
+ )
183
+ if method_name == :abandon_results!
184
+ @async_conn = nil
185
+ @async_sql = nil
186
+ end
187
+ wrap_mysql_result(result)
188
+ end
189
+
190
+ def ensure_mysql2!
191
+ require 'mysql2'
192
+ rescue LoadError
193
+ raise LoadError, "WrapperMysql2Client requires 'mysql2'. Add it to your Gemfile: gem 'mysql2'"
194
+ end
195
+
196
+ def current_conn
197
+ @service_container.connection_service.current_connection
198
+ end
199
+
200
+ def pm
201
+ @service_container.plugin_manager
202
+ end
203
+
204
+ def network_bound_methods
205
+ @network_bound_methods ||= @service_container.dialect_service.driver_dialect.network_bound_methods
206
+ end
207
+
208
+ def wrap_mysql_result(result, sql = nil)
209
+ return result unless result.is_a?(Mysql2::Result)
210
+
211
+ Mysql2WrapperResult.new(result, @service_container, current_conn, sql)
212
+ end
213
+ end
214
+
215
+ class Mysql2WrapperStatement
216
+ # @param sql [String, nil] the SQL the statement was prepared with, kept so that plugins
217
+ # which inspect statements still see it when the statement is executed
218
+ def initialize(service_container, connection, mysql_stmt, sql = nil)
219
+ @service_container = service_container
220
+ @connection = connection
221
+ @mysql_stmt = mysql_stmt
222
+ @sql = sql
223
+ end
224
+
225
+ def execute(*params, **)
226
+ result = pm.execute(
227
+ RubyMethod::STATEMENT_EXECUTE, current_conn,
228
+ ->(*p, **o) { @mysql_stmt.execute(*p, **o) },
229
+ *params, bounded_conn: @connection, sql: @sql, **
230
+ )
231
+ return result unless result.is_a?(Mysql2::Result)
232
+
233
+ # The statement is handed to the result so it can read column names from the statement's
234
+ # metadata rather than from Mysql2::Result#fields, which dereferences the result's field
235
+ # pointer and segfaults on a prepared statement that returned no rows.
236
+ Mysql2WrapperResult.new(result, @service_container, @connection, @sql, @mysql_stmt)
237
+ end
238
+
239
+ def close
240
+ pm.execute(RubyMethod::STATEMENT_CLOSE, current_conn, -> { @mysql_stmt.close })
241
+ end
242
+
243
+ # Delegate non-network methods directly
244
+ def fields
245
+ @mysql_stmt.fields
246
+ end
247
+
248
+ def field_count
249
+ @mysql_stmt.field_count
250
+ end
251
+
252
+ def param_count
253
+ @mysql_stmt.param_count
254
+ end
255
+
256
+ def affected_rows
257
+ @mysql_stmt.affected_rows
258
+ end
259
+
260
+ def last_id
261
+ @mysql_stmt.last_id
262
+ end
263
+
264
+ def closed?
265
+ @mysql_stmt.closed?
266
+ end
267
+
268
+ def inspect
269
+ format('#<%<class>s:0x%<addr>016x>', class: self.class.name, addr: object_id << 1)
270
+ end
271
+ alias to_s inspect
272
+
273
+ def pretty_print(pp)
274
+ pp.text(inspect)
275
+ end
276
+
277
+ private
278
+
279
+ def current_conn
280
+ @service_container.connection_service.current_connection
281
+ end
282
+
283
+ def pm
284
+ @service_container.plugin_manager
285
+ end
286
+ end
287
+
288
+ class Mysql2WrapperResult
289
+ include Enumerable
290
+
291
+ # @param sql [String, nil] the SQL that produced the result, kept so that plugins which
292
+ # inspect statements still see it when the rows are read
293
+ # @param statement [Mysql2::Statement, nil] the prepared statement the result came from, if any,
294
+ # used as a safe source of column names (see {#result_field_names})
295
+ def initialize(result, service_container, connection, sql = nil, statement = nil)
296
+ @result = result
297
+ @service_container = service_container
298
+ @connection = connection
299
+ @sql = sql
300
+ @statement = statement
301
+ end
302
+
303
+ def each(*args, &block)
304
+ pm.execute(RubyMethod::RESULT_EACH, current_conn, ->(&blk) { @result.each(*args, &blk) },
305
+ bounded_conn: @connection, sql: @sql, field_names: -> { result_field_names }, &block)
306
+ end
307
+
308
+ def to_a
309
+ pm.execute(RubyMethod::RESULT_TO_A, current_conn, -> { @result.to_a },
310
+ bounded_conn: @connection, sql: @sql, field_names: -> { result_field_names })
311
+ end
312
+
313
+ def [](index)
314
+ pm.execute(RubyMethod::RESULT_BRACKET, current_conn, ->(*a) { @result[*a] }, index,
315
+ bounded_conn: @connection, sql: @sql, field_names: -> { result_field_names })
316
+ end
317
+
318
+ # A buffered result is already in client memory, so letting it go is local. An unbuffered one,
319
+ # from +query(sql, stream: true)+, still has whatever was not read on the wire, and libmysql has
320
+ # to drain it before it can free the result. That makes this a call to the server, on the
321
+ # connection the statement was sent on, so it should go through the pipeline.
322
+ def free
323
+ pm.execute(RubyMethod::RESULT_FREE, current_conn, -> { @result.free }, bounded_conn: @connection, sql: @sql)
324
+ end
325
+
326
+ # Mysql2::Statement#execute reads a buffered result to the end before returning it, and when that
327
+ # read finds no rows mysql2 frees the result's metadata without having cached any column names.
328
+ # Mysql2::Result#fields then reads the freed metadata - an empty list at best, a segfault at worst
329
+ # (ActiveRecord calls it on every result, so any finder that matches nothing can crash). The
330
+ # names for that one case come from the statement's own metadata, symbolized when the result was
331
+ # asked for symbolize_keys just as Mysql2::Result#fields would. Every other result is read
332
+ # directly, so a freed streaming result still raises as the driver does.
333
+ def fields
334
+ return @result.fields unless empty_buffered_statement_result?
335
+
336
+ names = @statement.fields
337
+ result_query_options[:symbolize_keys] ? names.map(&:to_sym) : names
338
+ end
339
+
340
+ def field_types
341
+ @result.field_types
342
+ end
343
+
344
+ def count
345
+ @result.count
346
+ end
347
+
348
+ def size
349
+ @result.size
350
+ end
351
+
352
+ def server_flags
353
+ @result.server_flags
354
+ end
355
+
356
+ def query_time
357
+ @result.query_time
358
+ end
359
+
360
+ def tables
361
+ @result.tables
362
+ end
363
+
364
+ def dbs
365
+ @result.dbs
366
+ end
367
+
368
+ def method_missing(method_name, ...)
369
+ return @result.send(method_name, ...) if @result.respond_to?(method_name)
370
+
371
+ super
372
+ end
373
+
374
+ def respond_to_missing?(method_name, include_private = false)
375
+ @result.respond_to?(method_name, include_private) || super
376
+ end
377
+
378
+ def inspect
379
+ format('#<%<class>s:0x%<addr>016x>', class: self.class.name, addr: object_id << 1)
380
+ end
381
+ alias to_s inspect
382
+
383
+ def pretty_print(pp)
384
+ pp.text(inspect)
385
+ end
386
+
387
+ private
388
+
389
+ # Column names for the plugins that inspect a result as it is read, such as one that decrypts
390
+ # certain columns and has to know which position each holds. This is internal plumbing, not the
391
+ # driver's own #fields - so unlike #fields it must always yield the real column names, including
392
+ # for a prepared-statement result that fetched no rows. Mysql2::Result#fields cannot supply them
393
+ # there: it dereferences the result's field pointer, which mysql2 never populates until a row is
394
+ # fetched, so it returns an empty list or, on some client libraries, segfaults. In that one case
395
+ # the names come from the statement's own metadata (mysql_stmt_result_metadata), which carries
396
+ # them whether or not any rows came back. Once a row has been fetched the field cache is in place
397
+ # and the result's own fields are safe; results from +query+ carry no statement and read directly.
398
+ def result_field_names
399
+ # none? would iterate the result and consume an unbuffered one; count reads the driver's own
400
+ # row count without touching the rows.
401
+ return @statement.fields if @statement && @result.count.zero? # rubocop:disable Style/CollectionQuerying
402
+
403
+ @result.fields
404
+ end
405
+
406
+ # A streaming result is not read until the caller iterates it, so only a buffered one has had its
407
+ # metadata freed by the time it is returned. count reads the driver's own row count without
408
+ # touching the rows.
409
+ def empty_buffered_statement_result?
410
+ @statement && !result_query_options[:stream] && @result.count.zero? # rubocop:disable Style/CollectionQuerying
411
+ end
412
+
413
+ # Part of the workaround in #fields for a mysql2 bug that can segfault when an empty
414
+ # prepared-statement result is asked for its fields. Telling that case apart, and symbolizing the
415
+ # fallback names, needs the options the result was produced with; mysql2 keeps them in this ivar
416
+ # and exposes no reader for it.
417
+ def result_query_options
418
+ @result.instance_variable_get(:@query_options) || {}
419
+ end
420
+
421
+ def current_conn
422
+ @service_container.connection_service.current_connection
423
+ end
424
+
425
+ def pm
426
+ @service_container.plugin_manager
427
+ end
428
+ end
429
+ end