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,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