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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -2
  3. data/README.md +115 -6
  4. data/THIRD-PARTY-LICENSES +70 -11
  5. data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
  6. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb +40 -0
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_connection_handler.rb +47 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +23 -9
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +65 -10
  10. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +61 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +48 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -81
  16. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +28 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  20. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +11 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +14 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +6 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +125 -7
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +10 -13
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +95 -3
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +23 -1
  53. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +49 -23
  54. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  55. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  57. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  58. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +23 -4
  59. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  60. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  62. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +18 -7
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +29 -8
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/utils/{ar_constants.rb → telemetry/open_telemetry_counter.rb} +19 -5
  80. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  85. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  86. data/lib/aws_advanced_ruby_driver_wrapper.rb +47 -2
  87. metadata +57 -3
@@ -54,8 +54,41 @@ module AwsAdvancedRubyDriverWrapper
54
54
  type: String)
55
55
  DIALECT = WrapperProperty.new(:wrapper_dialect, 'The database dialect identifier for the database in use.', type: String)
56
56
  AWS_CREDENTIALS_PROVIDER = WrapperProperty.new(:aws_credentials_provider,
57
- 'AWS credentials provider for IAM token generation or Secrets Manager',
57
+ 'AWS credentials provider for the plugins that call AWS services',
58
58
  default_value: nil)
59
+ # -- Telemetry --
60
+ ENABLE_TELEMETRY = WrapperProperty.new(
61
+ :enable_telemetry,
62
+ 'Enables telemetry and observability of the wrapper.',
63
+ default_value: false,
64
+ type: :boolean
65
+ )
66
+ TELEMETRY_SUBMIT_TOP_LEVEL = WrapperProperty.new(
67
+ :telemetry_submit_top_level,
68
+ 'Submit the traces of wrapper calls as top-level traces, rather than only as children of a trace already in progress.',
69
+ default_value: false,
70
+ type: :boolean
71
+ )
72
+ TELEMETRY_TRACES_BACKEND = WrapperProperty.new(
73
+ :telemetry_traces_backend,
74
+ 'Method to export the telemetry traces of the wrapper. Valid values are otlp and none.',
75
+ default_value: nil,
76
+ type: String
77
+ )
78
+ TELEMETRY_METRICS_BACKEND = WrapperProperty.new(
79
+ :telemetry_metrics_backend,
80
+ 'Method to export the telemetry metrics of the wrapper. Valid values are otlp and none.',
81
+ default_value: nil,
82
+ type: String
83
+ )
84
+ TELEMETRY_FAILOVER_ADDITIONAL_TOP_TRACE = WrapperProperty.new(
85
+ :telemetry_failover_additional_top_trace,
86
+ 'Also submit the trace of each failover as a top-level trace of its own. Only a failover that was ' \
87
+ 'traced is submitted: one inside a trace in progress, or inside a wrapper call that was submitted ' \
88
+ 'as a top-level trace.',
89
+ default_value: false,
90
+ type: :boolean
91
+ )
59
92
  # -- Failover --
60
93
  FAILOVER_TIMEOUT_SEC = WrapperProperty.new(
61
94
  :failover_timeout_sec,
@@ -159,6 +192,15 @@ module AwsAdvancedRubyDriverWrapper
159
192
  default_value: 0.1, type: Float, validator: POSITIVE_FLOAT)
160
193
  BG_SWITCHOVER_TIMEOUT_SEC = WrapperProperty.new(:bg_switchover_timeout_sec, 'Blue/Green Deployment switchover timeout (in seconds).',
161
194
  default_value: 180.0, type: Float, validator: POSITIVE_FLOAT)
195
+ BG_DROP_BLUE_CONNECTIONS = WrapperProperty.new(
196
+ :bg_drop_blue_connections,
197
+ 'Whether to invalidate connections that were opened to the old blue environment once a Blue/Green ' \
198
+ 'switchover completes. When enabled, the next use of such a connection closes it: with the failover ' \
199
+ 'plugin enabled it reconnects through Blue/Green routing and raises FailoverSuccessError, and without ' \
200
+ 'failover the driver raises its closed-connection error. Either way the caller gets an explicit error ' \
201
+ 'instead of a stale read from old blue.',
202
+ default_value: true, type: :boolean
203
+ )
162
204
 
163
205
  # -- IAM Authentication --
164
206
  IAM_HOST = WrapperProperty.new(:iam_host, 'Overrides the host used to generate the IAM token', default_value: nil, type: String)
@@ -171,6 +213,26 @@ module AwsAdvancedRubyDriverWrapper
171
213
  IAM_ACCESS_TOKEN_PROPERTY_NAME = WrapperProperty.new(:iam_access_token_property_name, 'Property name used to pass the IAM token',
172
214
  default_value: :password, type: Symbol)
173
215
 
216
+ # -- Federated / Okta Authentication (SAML) --
217
+ IDP_ENDPOINT = WrapperProperty.new(:idp_endpoint, 'The hosting URL of the Identity Provider', default_value: nil, type: String)
218
+ IDP_PORT = WrapperProperty.new(:idp_port, 'The hosting port of the Identity Provider', default_value: 443, type: Integer)
219
+ IDP_USERNAME = WrapperProperty.new(:idp_username, 'The federated user name', default_value: nil, type: String)
220
+ IDP_PASSWORD = WrapperProperty.new(:idp_password, 'The federated user password', default_value: nil, type: String)
221
+ IAM_ROLE_ARN = WrapperProperty.new(:iam_role_arn, 'The ARN of the IAM Role that is to be assumed', default_value: nil, type: String)
222
+ IAM_IDP_ARN = WrapperProperty.new(:iam_idp_arn, 'The ARN of the Identity Provider', default_value: nil, type: String)
223
+ RP_IDENTIFIER = WrapperProperty.new(:rp_identifier, 'The relying party identifier', default_value: 'urn:amazon:webservices',
224
+ type: String)
225
+ DB_USER = WrapperProperty.new(:db_user, 'The database user used to access the database', default_value: nil, type: String)
226
+ SSL_INSECURE = WrapperProperty.new(:ssl_insecure, "Whether the IdP server's TLS certificate chain should not be verified",
227
+ default_value: false, type: :boolean)
228
+ APP_ID = WrapperProperty.new(:app_id, 'The ID of the AWS application configured on Okta', default_value: nil, type: String)
229
+ HTTP_CLIENT_SOCKET_TIMEOUT_SEC = WrapperProperty.new(:http_client_socket_timeout_sec,
230
+ 'Socket timeout in seconds for IdP HTTP requests',
231
+ default_value: 60.0, type: Float, validator: POSITIVE_FLOAT)
232
+ HTTP_CLIENT_CONNECT_TIMEOUT_SEC = WrapperProperty.new(:http_client_connect_timeout_sec,
233
+ 'Connect timeout in seconds for IdP HTTP requests',
234
+ default_value: 60.0, type: Float, validator: POSITIVE_FLOAT)
235
+
174
236
  # -- Initial Connection Strategy --
175
237
  INITIAL_CONNECTION_SUBSTITUTE_HOST = WrapperProperty.new(
176
238
  :initial_connection_substitute_host,
@@ -46,6 +46,9 @@ module AwsAdvancedRubyDriverWrapper
46
46
  CONNECTION_ABANDON_RESULTS = define('connection.abandon_results!', check_bounded_connection: true)
47
47
  CONNECTION_SET_SERVER_OPTION = define('connection.set_server_option', check_bounded_connection: false)
48
48
 
49
+ # -- Connection methods (trilogy-specific) --
50
+ CONNECTION_CHANGE_DB = define('connection.change_db', check_bounded_connection: false)
51
+
49
52
  # -- Connection methods (pg-specific) --
50
53
  CONNECTION_EXEC = define('connection.exec', check_bounded_connection: false)
51
54
  CONNECTION_ASYNC_EXEC = define('connection.async_exec', check_bounded_connection: false)
@@ -61,7 +61,7 @@ module AwsAdvancedRubyDriverWrapper
61
61
  }.freeze
62
62
 
63
63
  # @param connection_service [ConnectionService]
64
- # @param driver_name [Symbol] :mysql2 or :postgresql
64
+ # @param driver_name [Symbol] :mysql2, :trilogy or :postgresql
65
65
  def initialize(connection_service, driver_name)
66
66
  @connection_service = connection_service
67
67
  @dialect_cache = {}
@@ -164,6 +164,21 @@ module AwsAdvancedRubyDriverWrapper
164
164
  @error_handler.read_only_error?(error)
165
165
  end
166
166
 
167
+ # Whether the driver reported the error as a database error (it carries a SQLSTATE, or for
168
+ # trilogy a MySQL error code). Not a strict server-vs-client split; see ErrorHandler#database_error?.
169
+ # @param error [Exception]
170
+ # @return [Boolean]
171
+ def database_error?(error)
172
+ @error_handler.database_error?(error)
173
+ end
174
+
175
+ # Whether the error is a schema/syntax error (SQLSTATE class '42' or its per-driver equivalent).
176
+ # @param error [Exception]
177
+ # @return [Boolean]
178
+ def schema_error?(error)
179
+ @error_handler.schema_error?(error)
180
+ end
181
+
167
182
  private
168
183
 
169
184
  # Resolves the initial database dialect from the initial connection info.
@@ -205,12 +220,10 @@ module AwsAdvancedRubyDriverWrapper
205
220
 
206
221
  rds_type = Utils::RdsUtils.identify_rds_type(host)
207
222
 
208
- @dialect_code = if @driver_dialect == DriverDialects::DriverDialectManager::MYSQL_DIALECT
209
- resolve_mysql_dialect(rds_type)
210
- elsif @driver_dialect == DriverDialects::DriverDialectManager::PG_DIALECT
211
- resolve_pg_dialect(rds_type)
212
- else
213
- DialectCodes::UNKNOWN
223
+ @dialect_code = case @driver_dialect.database_engine
224
+ when :mysql then resolve_mysql_dialect(rds_type)
225
+ when :postgresql then resolve_pg_dialect(rds_type)
226
+ else DialectCodes::UNKNOWN
214
227
  end
215
228
 
216
229
  @db_dialect = dialect_for_code(@dialect_code)
@@ -15,6 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  require 'concurrent/map'
18
+ require_relative '../errors'
18
19
  require_relative '../host/random_host_selector'
19
20
 
20
21
  module AwsAdvancedRubyDriverWrapper
@@ -47,18 +48,45 @@ module AwsAdvancedRubyDriverWrapper
47
48
  # Register a non-default host selector. The selector is shared by every HostService in the process,
48
49
  # so it must be safe to call from multiple threads.
49
50
  #
51
+ # Strategy names are case-insensitive: they are normalized here and in #host_selector, so a
52
+ # selector registered as 'LeastLag' is found when a connection names 'leastlag'.
53
+ #
50
54
  # @param name [String] strategy name
51
55
  # @param selector [#select_host] any object responding to select_host(hosts, role, props)
52
56
  def register_host_selector(name, selector)
53
- raise Errors::AwsError, "Cannot override default host selection strategy: '#{name}'" if DEFAULT_HOST_SELECTORS.key?(name)
57
+ normalized = normalize_strategy(name)
58
+ raise Errors::AwsError, "Cannot override default host selection strategy: '#{name}'" if DEFAULT_HOST_SELECTORS.key?(normalized)
54
59
 
55
- @strategies[name] = selector
60
+ @strategies[normalized] = selector
56
61
  end
57
62
 
58
63
  # @param name [String] strategy name
59
64
  # @return [#select_host, nil] the registered selector, or nil if the name is unknown
60
65
  def host_selector(name)
61
- @strategies[name]
66
+ @strategies[normalize_strategy(name)]
67
+ end
68
+
69
+ # Raises when no selector is registered under the given strategy name. Plugins call this when
70
+ # they are constructed, because they treat a selection failure during connect or failover as
71
+ # "no eligible host", which would otherwise hide a misspelled strategy.
72
+ #
73
+ # @param name [String] strategy name
74
+ def validate_strategy!(name)
75
+ raise unsupported_strategy_error(name) if host_selector(name).nil?
76
+ end
77
+
78
+ def normalize_strategy(name)
79
+ name.to_s.strip.downcase
80
+ end
81
+
82
+ # @param name [String] the strategy name as configured, reported without normalization
83
+ # @return [Errors::AwsError]
84
+ def unsupported_strategy_error(name)
85
+ Errors::AwsError.new(
86
+ "Unsupported host selection strategy: '#{name}'. Registered strategies: #{@strategies.keys.sort.join(', ')}. " \
87
+ 'Register a custom strategy with AwsAdvancedRubyDriverWrapper::Services::HostService.register_host_selector ' \
88
+ 'before opening a connection that uses it.'
89
+ )
62
90
  end
63
91
 
64
92
  # Removes every non-default host selector. For testing only.
@@ -76,9 +104,8 @@ module AwsAdvancedRubyDriverWrapper
76
104
  # @param props [Hash, nil]
77
105
  # @return [Host::HostInfo]
78
106
  def select_host(hosts, role, strategy, props = nil)
79
- normalized = strategy.to_s.strip.downcase
80
- selector = self.class.host_selector(normalized)
81
- raise Errors::AwsError, "Unsupported host selection strategy: '#{strategy}'" if selector.nil?
107
+ selector = self.class.host_selector(strategy)
108
+ raise self.class.unsupported_strategy_error(strategy) if selector.nil?
82
109
 
83
110
  selector.select_host(hosts, role, props)
84
111
  end
@@ -90,8 +117,7 @@ module AwsAdvancedRubyDriverWrapper
90
117
  def hosts
91
118
  rules = @service_container.storage_service.get_if_registered(
92
119
  :custom_endpoint_allowed_blocked,
93
- @service_container.connection_service.initial_host_info&.url,
94
- register_access: false
120
+ @service_container.connection_service.initial_host_info&.url
95
121
  )
96
122
  return @all_hosts if rules.nil?
97
123
 
@@ -14,6 +14,7 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'concurrent'
17
18
  require_relative '../monitoring/monitor_state'
18
19
  require_relative '../logging'
19
20
  require_relative '../utils/storage/sliding_expiration_cache'
@@ -36,7 +37,7 @@ module AwsAdvancedRubyDriverWrapper
36
37
  def initialize(event_publisher:)
37
38
  @caches = {}
38
39
  @lock = Mutex.new
39
- @running = true
40
+ @running = Concurrent::AtomicBoolean.new(true)
40
41
  @cleanup_thread = start_cleanup_thread
41
42
  event_publisher.subscribe(
42
43
  self,
@@ -112,7 +113,7 @@ module AwsAdvancedRubyDriverWrapper
112
113
  end
113
114
 
114
115
  def shutdown(grace_period:)
115
- @running = false
116
+ @running.make_false
116
117
  begin
117
118
  @cleanup_thread&.wakeup
118
119
  rescue ThreadError
@@ -122,10 +123,28 @@ module AwsAdvancedRubyDriverWrapper
122
123
  stop_and_remove_all
123
124
  end
124
125
 
126
+ # Resets this service in a forked child. Monitor threads do not survive a fork, so the inherited
127
+ # monitors are detached (keeping the parent's connections open) and forgotten, letting
128
+ # run_if_absent start live ones. A monitor that fails to detach is still forgotten. The cleanup
129
+ # thread is restarted unless it is still alive, so calling this again does not start a second one.
130
+ def restart_after_fork
131
+ @lock.synchronize { @caches.values }.each do |container|
132
+ container.cache.entries.each_key do |key|
133
+ container.cache.remove(key)&.release_after_fork
134
+ rescue StandardError => e
135
+ logger.warn("Failed to release monitor #{key} after fork: #{e.message}")
136
+ end
137
+ end
138
+ return if @cleanup_thread&.alive?
139
+
140
+ @running.make_true
141
+ @cleanup_thread = start_cleanup_thread
142
+ end
143
+
125
144
  # Processes events from the event publisher.
126
145
  # @param event [Event] the event to process.
127
146
  def process_event(event)
128
- return unless event == Utils::Events::DataAccessEvent
147
+ return unless event.is_a?(Utils::Events::DataAccessEvent)
129
148
 
130
149
  handle_data_access_event(event)
131
150
  end
@@ -142,7 +161,7 @@ module AwsAdvancedRubyDriverWrapper
142
161
 
143
162
  def start_cleanup_thread
144
163
  thread = Thread.new do
145
- while @running
164
+ while @running.true?
146
165
  sleep(CLEANUP_INTERVAL_SEC)
147
166
  run_cleanup
148
167
  end
@@ -19,10 +19,13 @@ require_relative '../errors'
19
19
  require_relative '../property_definition'
20
20
  require_relative '../ruby_method'
21
21
  require_relative '../utils/sql_encoding'
22
+ require_relative '../utils/telemetry/telemetry_const'
22
23
  require_relative '../plugins/default_plugin'
23
24
  require_relative '../plugins/failover_plugin'
24
25
  require_relative '../plugins/gdb/gdb_failover_plugin'
25
26
  require_relative '../plugins/iam_auth_plugin'
27
+ require_relative '../plugins/federated_auth/federated_auth_plugin'
28
+ require_relative '../plugins/federated_auth/okta_auth_plugin'
26
29
  require_relative '../plugins/initial_connection_strategy_plugin'
27
30
  require_relative '../plugins/kms_encryption/kms_encryption_plugin'
28
31
  require_relative '../plugins/secrets_manager_plugin'
@@ -44,6 +47,8 @@ module AwsAdvancedRubyDriverWrapper
44
47
  'failover' => Plugins::FailoverPlugin,
45
48
  'gdb_failover' => Plugins::Gdb::GdbFailoverPlugin,
46
49
  'iam' => Plugins::IamAuthPlugin,
50
+ 'federated_auth' => Plugins::FederatedAuth::FederatedAuthPlugin,
51
+ 'okta' => Plugins::FederatedAuth::OktaAuthPlugin,
47
52
  'initial_connection' => Plugins::InitialConnectionStrategyPlugin,
48
53
  'kms_encryption' => Plugins::KmsEncryptionPlugin,
49
54
  'secrets_manager' => Plugins::SecretsManagerPlugin
@@ -60,9 +65,18 @@ module AwsAdvancedRubyDriverWrapper
60
65
  Plugins::Gdb::GdbFailoverPlugin => 500,
61
66
  Plugins::IamAuthPlugin => 1800,
62
67
  Plugins::SecretsManagerPlugin => 1900,
68
+ Plugins::FederatedAuth::FederatedAuthPlugin => 2000,
69
+ Plugins::FederatedAuth::OktaAuthPlugin => 2010,
63
70
  Plugins::KmsEncryptionPlugin => 2050
64
71
  }
65
72
 
73
+ # Plugins that authenticate by minting an RDS IAM token into the shared IAM token cache.
74
+ IAM_TOKEN_PLUGIN_CLASSES = [
75
+ Plugins::IamAuthPlugin,
76
+ Plugins::FederatedAuth::FederatedAuthPlugin,
77
+ Plugins::FederatedAuth::OktaAuthPlugin
78
+ ].freeze
79
+
66
80
  class << self
67
81
  def register_plugin(plugin_code, plugin_class, weight: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN)
68
82
  @plugin_classes[plugin_code] = plugin_class
@@ -73,30 +87,35 @@ module AwsAdvancedRubyDriverWrapper
73
87
  end
74
88
 
75
89
  def initialize(service_container)
90
+ @telemetry_factory = service_container.telemetry_factory
76
91
  @plugins = load_plugins(service_container)
77
92
  @pipeline_cache = {}
78
93
  end
79
94
 
80
95
  def connect(host_info, driver_props, is_initial_connection, plugin_to_skip: nil)
81
- execute_with_subscribed_plugins(
82
- 'connect',
83
- lambda do |plugin, next_plugin_callable|
84
- plugin.connect(host_info, driver_props, is_initial_connection, next_plugin_callable)
85
- end,
86
- NOOP_CALLABLE,
87
- plugin_to_skip:
88
- )
96
+ @telemetry_factory.in_context('connect', Utils::Telemetry::TraceLevel::NESTED) do
97
+ execute_with_subscribed_plugins(
98
+ 'connect',
99
+ lambda do |plugin, next_plugin_callable|
100
+ plugin.connect(host_info, driver_props, is_initial_connection, next_plugin_callable)
101
+ end,
102
+ NOOP_CALLABLE,
103
+ plugin_to_skip:
104
+ )
105
+ end
89
106
  end
90
107
 
91
108
  def internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, plugin_to_skip: nil)
92
- execute_with_subscribed_plugins(
93
- 'internal_connect',
94
- lambda do |plugin, next_plugin_callable|
95
- plugin.internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, next_plugin_callable)
96
- end,
97
- NOOP_CALLABLE,
98
- plugin_to_skip:
99
- )
109
+ @telemetry_factory.in_context('internal_connect', Utils::Telemetry::TraceLevel::NESTED) do
110
+ execute_with_subscribed_plugins(
111
+ 'internal_connect',
112
+ lambda do |plugin, next_plugin_callable|
113
+ plugin.internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, next_plugin_callable)
114
+ end,
115
+ NOOP_CALLABLE,
116
+ plugin_to_skip:
117
+ )
118
+ end
100
119
  end
101
120
 
102
121
  # The context of the call currently being executed, for plugins that need to know more about
@@ -140,15 +159,18 @@ module AwsAdvancedRubyDriverWrapper
140
159
  Thread.current[CURRENT_CALL_CONTEXT_KEY] = context
141
160
 
142
161
  begin
143
- execute_with_subscribed_plugins(
144
- method_name,
145
- lambda do |plugin, next_plugin_callable|
146
- # Read from the context rather than from args and block, so that a plugin which
147
- # replaced either is honoured by the plugins after it and by the target method.
148
- plugin.execute(method_name, next_plugin_callable, *context.args, **kwargs, &context.block)
149
- end,
150
- target_callable
151
- )
162
+ @telemetry_factory.in_context(method_name, Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do |telemetry_context|
163
+ telemetry_context.set_attribute(Utils::Telemetry::TelemetryConst::RUBY_CALL_ANNOTATION, method_name)
164
+ execute_with_subscribed_plugins(
165
+ method_name,
166
+ lambda do |plugin, next_plugin_callable|
167
+ # Read from the context rather than from args and block, so that a plugin which
168
+ # replaced either is honoured by the plugins after it and by the target method.
169
+ plugin.execute(method_name, next_plugin_callable, *context.args, **kwargs, &context.block)
170
+ end,
171
+ target_callable
172
+ )
173
+ end
152
174
  ensure
153
175
  Thread.current[CURRENT_CALL_CONTEXT_KEY] = previous_context
154
176
  end
@@ -164,6 +186,12 @@ module AwsAdvancedRubyDriverWrapper
164
186
  @plugins.any?(plugin_class)
165
187
  end
166
188
 
189
+ # Whether any plugin that mints RDS IAM tokens into the shared cache is active.
190
+ # @see IAM_TOKEN_PLUGIN_CLASSES
191
+ def iam_token_plugin_in_use?
192
+ IAM_TOKEN_PLUGIN_CLASSES.any? { |plugin_class| plugin_in_use?(plugin_class) }
193
+ end
194
+
167
195
  private
168
196
 
169
197
  def load_plugins(service_container)
@@ -185,7 +213,7 @@ module AwsAdvancedRubyDriverWrapper
185
213
  end
186
214
 
187
215
  def ensure_single_auth_plugin(plugin_code_list)
188
- auth_plugins_used = plugin_code_list & %w[iam secrets_manager].freeze
216
+ auth_plugins_used = plugin_code_list & %w[iam secrets_manager federated_auth okta].freeze
189
217
 
190
218
  return unless auth_plugins_used.length > 1
191
219
 
@@ -245,29 +273,52 @@ module AwsAdvancedRubyDriverWrapper
245
273
 
246
274
  # Builds the plugin pipeline function chain. The pipeline allows plugins to perform logic both before and after
247
275
  # the target driver function is called.
276
+ #
277
+ # When traces are recorded, each plugin's part of the call is recorded as a span named after the
278
+ # plugin. That is decided here, once per pipeline, so a pipeline built without traces does no
279
+ # tracing work at all.
248
280
  def make_pipeline(target_method_name)
249
281
  subscribed_plugins = @plugins.select do |p|
250
282
  p.subscribed_methods.include?('*') || p.subscribed_methods.include?(target_method_name)
251
283
  end
252
284
  raise Errors::AwsError, 'Plugin pipeline is nil' if subscribed_plugins.empty?
253
285
 
286
+ traced = @telemetry_factory.traces_in_use?
254
287
  base_plugin = subscribed_plugins.last
288
+ base_plugin_name = plugin_name(base_plugin) if traced
255
289
  base = lambda do |plugin_callable, target_driver_callable, *, **|
256
- plugin_callable.call(base_plugin, target_driver_callable)
290
+ if traced
291
+ call_traced(plugin_callable, base_plugin, base_plugin_name, target_driver_callable)
292
+ else
293
+ plugin_callable.call(base_plugin, target_driver_callable)
294
+ end
257
295
  end
258
296
 
259
297
  subscribed_plugins[0...-1].reverse.reduce(base) do |next_plugin_callable, plugin|
298
+ name = plugin_name(plugin) if traced
260
299
  lambda do |plugin_callable, target_driver_callable, method_name, plugin_to_skip|
261
300
  if plugin_to_skip == plugin
262
301
  next_plugin_callable.call(plugin_callable, target_driver_callable, method_name, plugin_to_skip)
263
302
  else
264
- plugin_callable.call(plugin, lambda do
303
+ call_next = lambda do
265
304
  next_plugin_callable.call(plugin_callable, target_driver_callable, method_name, plugin_to_skip)
266
- end)
305
+ end
306
+ traced ? call_traced(plugin_callable, plugin, name, call_next) : plugin_callable.call(plugin, call_next)
267
307
  end
268
308
  end
269
309
  end
270
310
  end
311
+
312
+ # A plugin's span is not marked as failed when the call raises: the error passes out through every
313
+ # plugin's span, and is recorded on the call's top-level span instead.
314
+ def call_traced(plugin_callable, plugin, name, next_plugin_callable)
315
+ @telemetry_factory.in_context(name, Utils::Telemetry::TraceLevel::NESTED) { plugin_callable.call(plugin, next_plugin_callable) }
316
+ end
317
+
318
+ # The plugin's code (such as +failover+), or the class name of a plugin that has none.
319
+ def plugin_name(plugin)
320
+ self.class.plugin_classes.key(plugin.class) || plugin.class.name&.split('::')&.last || plugin.class.to_s
321
+ end
271
322
  end
272
323
  end
273
324
  end
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative '../utils/telemetry/null_telemetry'
18
+
17
19
  module AwsAdvancedRubyDriverWrapper
18
20
  module Services
19
21
  ServiceContainer = Struct.new(
@@ -24,7 +26,13 @@ module AwsAdvancedRubyDriverWrapper
24
26
  :plugin_manager,
25
27
  :session_state_service,
26
28
  :storage_service,
27
- :monitor_service
28
- )
29
+ :monitor_service,
30
+ :telemetry_factory
31
+ ) do
32
+ # A container that was given no telemetry factory records no telemetry.
33
+ def telemetry_factory
34
+ self[:telemetry_factory] || Utils::Telemetry::NullTelemetryFactory::INSTANCE
35
+ end
36
+ end
29
37
  end
30
38
  end
@@ -23,6 +23,7 @@ require_relative 'monitor_service'
23
23
  require_relative 'session_state_service'
24
24
  require_relative '../utils/storage/storage_service'
25
25
  require_relative '../utils/events/batching_event_publisher'
26
+ require_relative '../utils/telemetry/telemetry_factory'
26
27
 
27
28
  module AwsAdvancedRubyDriverWrapper
28
29
  module Services
@@ -50,9 +51,10 @@ module AwsAdvancedRubyDriverWrapper
50
51
  end
51
52
 
52
53
  module ServiceUtility
53
- def self.create_standard_container(config)
54
+ def self.create_standard_container(config, telemetry_factory: nil)
54
55
  container = ServiceContainer.new
55
56
  container.event_publisher = CoreServices.event_publisher
57
+ container.telemetry_factory = telemetry_factory || Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
56
58
  container.connection_service = ConnectionService.new(container, config)
57
59
  container.dialect_service = DialectService.new(container.connection_service, config.driver_name)
58
60
  container.host_service = HostService.new(container)
@@ -71,6 +73,7 @@ module AwsAdvancedRubyDriverWrapper
71
73
  container.event_publisher = parent_container.event_publisher
72
74
  container.dialect_service = parent_container.dialect_service
73
75
  container.host_service = parent_container.host_service
76
+ container.telemetry_factory = parent_container.telemetry_factory
74
77
  container
75
78
  end
76
79
  end