opensearch-transport 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 (44) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +3 -0
  3. data/.gitignore +17 -0
  4. data/Gemfile +47 -0
  5. data/LICENSE +202 -0
  6. data/README.md +551 -0
  7. data/Rakefile +89 -0
  8. data/lib/opensearch/transport/client.rb +354 -0
  9. data/lib/opensearch/transport/redacted.rb +84 -0
  10. data/lib/opensearch/transport/transport/base.rb +450 -0
  11. data/lib/opensearch/transport/transport/connections/collection.rb +136 -0
  12. data/lib/opensearch/transport/transport/connections/connection.rb +169 -0
  13. data/lib/opensearch/transport/transport/connections/selector.rb +101 -0
  14. data/lib/opensearch/transport/transport/errors.rb +100 -0
  15. data/lib/opensearch/transport/transport/http/curb.rb +140 -0
  16. data/lib/opensearch/transport/transport/http/faraday.rb +101 -0
  17. data/lib/opensearch/transport/transport/http/manticore.rb +188 -0
  18. data/lib/opensearch/transport/transport/loggable.rb +94 -0
  19. data/lib/opensearch/transport/transport/response.rb +46 -0
  20. data/lib/opensearch/transport/transport/serializer/multi_json.rb +62 -0
  21. data/lib/opensearch/transport/transport/sniffer.rb +111 -0
  22. data/lib/opensearch/transport/version.rb +31 -0
  23. data/lib/opensearch/transport.rb +46 -0
  24. data/lib/opensearch-transport.rb +27 -0
  25. data/opensearch-transport.gemspec +92 -0
  26. data/spec/opensearch/connections/collection_spec.rb +275 -0
  27. data/spec/opensearch/connections/selector_spec.rb +183 -0
  28. data/spec/opensearch/transport/base_spec.rb +313 -0
  29. data/spec/opensearch/transport/client_spec.rb +1818 -0
  30. data/spec/opensearch/transport/sniffer_spec.rb +284 -0
  31. data/spec/spec_helper.rb +99 -0
  32. data/test/integration/transport_test.rb +108 -0
  33. data/test/profile/client_benchmark_test.rb +141 -0
  34. data/test/test_helper.rb +97 -0
  35. data/test/unit/connection_test.rb +145 -0
  36. data/test/unit/response_test.rb +41 -0
  37. data/test/unit/serializer_test.rb +42 -0
  38. data/test/unit/transport_base_test.rb +673 -0
  39. data/test/unit/transport_curb_test.rb +143 -0
  40. data/test/unit/transport_faraday_test.rb +237 -0
  41. data/test/unit/transport_manticore_test.rb +191 -0
  42. data.tar.gz.sig +1 -0
  43. metadata +456 -0
  44. metadata.gz.sig +1 -0
@@ -0,0 +1,450 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+
31
+ # @abstract Module with common functionality for transport implementations.
32
+ #
33
+ module Base
34
+ include Loggable
35
+
36
+ DEFAULT_PORT = 9200
37
+ DEFAULT_PROTOCOL = 'http'
38
+ DEFAULT_RELOAD_AFTER = 10_000 # Requests
39
+ DEFAULT_RESURRECT_AFTER = 60 # Seconds
40
+ DEFAULT_MAX_RETRIES = 3 # Requests
41
+ DEFAULT_SERIALIZER_CLASS = Serializer::MultiJson
42
+ SANITIZED_PASSWORD = '*' * (rand(14)+1)
43
+
44
+ attr_reader :hosts, :options, :connections, :counter, :last_request_at, :protocol
45
+ attr_accessor :serializer, :sniffer, :logger, :tracer,
46
+ :reload_connections, :reload_after,
47
+ :resurrect_after
48
+
49
+ # Creates a new transport object
50
+ #
51
+ # @param arguments [Hash] Settings and options for the transport
52
+ # @param block [Proc] Lambda or Proc which can be evaluated in the context of the "session" object
53
+ #
54
+ # @option arguments [Array] :hosts An Array of normalized hosts information
55
+ # @option arguments [Array] :options A Hash with options (usually passed by {Client})
56
+ #
57
+ # @see Client#initialize
58
+ #
59
+ def initialize(arguments = {}, &block)
60
+ @state_mutex = Mutex.new
61
+
62
+ @hosts = arguments[:hosts] || []
63
+ @options = arguments[:options] || {}
64
+ @options[:http] ||= {}
65
+ @options[:retry_on_status] ||= []
66
+
67
+ @block = block
68
+ @compression = !!@options[:compression]
69
+ @connections = __build_connections
70
+
71
+ @serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self) )
72
+ @protocol = options[:protocol] || DEFAULT_PROTOCOL
73
+
74
+ @logger = options[:logger]
75
+ @tracer = options[:tracer]
76
+
77
+ @sniffer = options[:sniffer_class] ? options[:sniffer_class].new(self) : Sniffer.new(self)
78
+ @counter = 0
79
+ @counter_mtx = Mutex.new
80
+ @last_request_at = Time.now
81
+ @reload_connections = options[:reload_connections]
82
+ @reload_after = options[:reload_connections].is_a?(Integer) ? options[:reload_connections] : DEFAULT_RELOAD_AFTER
83
+ @resurrect_after = options[:resurrect_after] || DEFAULT_RESURRECT_AFTER
84
+ @retry_on_status = Array(options[:retry_on_status]).map { |d| d.to_i }
85
+ end
86
+
87
+ # Returns a connection from the connection pool by delegating to {Connections::Collection#get_connection}.
88
+ #
89
+ # Resurrects dead connection if the `resurrect_after` timeout has passed.
90
+ # Increments the counter and performs connection reloading if the `reload_connections` option is set.
91
+ #
92
+ # @return [Connections::Connection]
93
+ # @see Connections::Collection#get_connection
94
+ #
95
+ def get_connection(options={})
96
+ resurrect_dead_connections! if Time.now > @last_request_at + @resurrect_after
97
+
98
+ @counter_mtx.synchronize { @counter += 1 }
99
+ reload_connections! if reload_connections && counter % reload_after == 0
100
+ connections.get_connection(options)
101
+ end
102
+
103
+ # Reloads and replaces the connection collection based on cluster information
104
+ #
105
+ # @see Sniffer#hosts
106
+ #
107
+ def reload_connections!
108
+ hosts = sniffer.hosts
109
+ __rebuild_connections :hosts => hosts, :options => options
110
+ self
111
+ rescue SnifferTimeoutError
112
+ log_error "[SnifferTimeoutError] Timeout when reloading connections."
113
+ self
114
+ end
115
+
116
+ # Tries to "resurrect" all eligible dead connections
117
+ #
118
+ # @see Connections::Connection#resurrect!
119
+ #
120
+ def resurrect_dead_connections!
121
+ connections.dead.each { |c| c.resurrect! }
122
+ end
123
+
124
+ # Rebuilds the connections collection in the transport.
125
+ #
126
+ # The methods *adds* new connections from the passed hosts to the collection,
127
+ # and *removes* all connections not contained in the passed hosts.
128
+ #
129
+ # @return [Connections::Collection]
130
+ # @api private
131
+ #
132
+ def __rebuild_connections(arguments={})
133
+ @state_mutex.synchronize do
134
+ @hosts = arguments[:hosts] || []
135
+ @options = arguments[:options] || {}
136
+
137
+ __close_connections
138
+
139
+ new_connections = __build_connections
140
+ stale_connections = @connections.all.select { |c| ! new_connections.include?(c) }
141
+ new_connections = new_connections.reject { |c| @connections.all.include?(c) }
142
+
143
+ @connections.remove(stale_connections)
144
+ @connections.add(new_connections)
145
+ @connections
146
+ end
147
+ end
148
+
149
+ # Builds and returns a collection of connections
150
+ #
151
+ # The adapters have to implement the {Base#__build_connection} method.
152
+ #
153
+ # @return [Connections::Collection]
154
+ # @api private
155
+ #
156
+ def __build_connections
157
+ Connections::Collection.new \
158
+ :connections => hosts.map { |host|
159
+ host[:protocol] = host[:scheme] || options[:scheme] || options[:http][:scheme] || DEFAULT_PROTOCOL
160
+ host[:port] ||= options[:port] || options[:http][:port] || DEFAULT_PORT
161
+ if (options[:user] || options[:http][:user]) && !host[:user]
162
+ host[:user] ||= options[:user] || options[:http][:user]
163
+ host[:password] ||= options[:password] || options[:http][:password]
164
+ end
165
+
166
+ __build_connection(host, (options[:transport_options] || {}), @block)
167
+ },
168
+ :selector_class => options[:selector_class],
169
+ :selector => options[:selector]
170
+ end
171
+
172
+ # @abstract Build and return a connection.
173
+ # A transport implementation *must* implement this method.
174
+ # See {HTTP::Faraday#__build_connection} for an example.
175
+ #
176
+ # @return [Connections::Connection]
177
+ # @api private
178
+ #
179
+ def __build_connection(host, options={}, block=nil)
180
+ raise NoMethodError, "Implement this method in your class"
181
+ end
182
+
183
+ # Closes the connections collection
184
+ #
185
+ # @api private
186
+ #
187
+ def __close_connections
188
+ # A hook point for specific adapters when they need to close connections
189
+ end
190
+
191
+ # Log request and response information
192
+ #
193
+ # @api private
194
+ #
195
+ def __log_response(method, path, params, body, url, response, json, took, duration)
196
+ if logger
197
+ sanitized_url = url.to_s.gsub(/\/\/(.+):(.+)@/, '//' + '\1:' + SANITIZED_PASSWORD + '@')
198
+ log_info "#{method.to_s.upcase} #{sanitized_url} " +
199
+ "[status:#{response.status}, request:#{sprintf('%.3fs', duration)}, query:#{took}]"
200
+ log_debug "> #{__convert_to_json(body)}" if body
201
+ log_debug "< #{response.body}"
202
+ end
203
+ end
204
+
205
+ # Trace the request in the `curl` format
206
+ #
207
+ # @api private
208
+ #
209
+ def __trace(method, path, params, headers, body, url, response, json, took, duration)
210
+ trace_url = "http://localhost:9200/#{path}?pretty" +
211
+ ( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
212
+ trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
213
+ trace_command = "curl -X #{method.to_s.upcase}"
214
+ trace_command += " -H '#{headers.collect { |k,v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
215
+ trace_command += " '#{trace_url}'#{trace_body}\n"
216
+ tracer.info trace_command
217
+ tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
218
+ tracer.debug json ? serializer.dump(json, :pretty => true).gsub(/^/, '# ').sub(/\}$/, "\n# }")+"\n" : "# #{response.body}\n"
219
+ end
220
+
221
+ # Raise error specific for the HTTP response status or a generic server error
222
+ #
223
+ # @api private
224
+ #
225
+ def __raise_transport_error(response)
226
+ error = ERRORS[response.status] || ServerError
227
+ raise error.new "[#{response.status}] #{response.body}"
228
+ end
229
+
230
+ # Converts any non-String object to JSON
231
+ #
232
+ # @api private
233
+ #
234
+ def __convert_to_json(o=nil, options={})
235
+ o = o.is_a?(String) ? o : serializer.dump(o, options)
236
+ end
237
+
238
+ # Returns a full URL based on information from host
239
+ #
240
+ # @param host [Hash] Host configuration passed in from {Client}
241
+ #
242
+ # @api private
243
+ def __full_url(host)
244
+ url = "#{host[:protocol]}://"
245
+ url += "#{CGI.escape(host[:user])}:#{CGI.escape(host[:password])}@" if host[:user]
246
+ url += host[:host]
247
+ url += ":#{host[:port]}" if host[:port]
248
+ url += host[:path] if host[:path]
249
+ url
250
+ end
251
+
252
+ # Performs a request to Elasticsearch, while handling logging, tracing, marking dead connections,
253
+ # retrying the request and reloading the connections.
254
+ #
255
+ # @abstract The transport implementation has to implement this method either in full,
256
+ # or by invoking this method with a block. See {HTTP::Faraday#perform_request} for an example.
257
+ #
258
+ # @param method [String] Request method
259
+ # @param path [String] The API endpoint
260
+ # @param params [Hash] Request parameters (will be serialized by {Connections::Connection#full_url})
261
+ # @param body [Hash] Request body (will be serialized by the {#serializer})
262
+ # @param headers [Hash] Request headers (will be serialized by the {#serializer})
263
+ # @param block [Proc] Code block to evaluate, passed from the implementation
264
+ #
265
+ # @return [Response]
266
+ # @raise [NoMethodError] If no block is passed
267
+ # @raise [ServerError] If request failed on server
268
+ # @raise [Error] If no connection is available
269
+ #
270
+ def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {}, &block)
271
+ raise NoMethodError, 'Implement this method in your transport class' unless block_given?
272
+
273
+ start = Time.now
274
+ tries = 0
275
+ reload_on_failure = opts.fetch(:reload_on_failure, @options[:reload_on_failure])
276
+
277
+ max_retries = if opts.key?(:retry_on_failure)
278
+ opts[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : opts[:retry_on_failure]
279
+ elsif options.key?(:retry_on_failure)
280
+ options[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : options[:retry_on_failure]
281
+ end
282
+
283
+ params = params.clone
284
+
285
+ ignore = Array(params.delete(:ignore)).compact.map { |s| s.to_i }
286
+
287
+ begin
288
+ tries += 1
289
+ connection = get_connection or raise Error.new('Cannot get new connection from pool.')
290
+
291
+ if connection.connection.respond_to?(:params) && connection.connection.params.respond_to?(:to_hash)
292
+ params = connection.connection.params.merge(params.to_hash)
293
+ end
294
+
295
+ url = connection.full_url(path, params)
296
+
297
+ response = block.call(connection, url)
298
+
299
+ connection.healthy! if connection.failures > 0
300
+
301
+ # Raise an exception so we can catch it for `retry_on_status`
302
+ __raise_transport_error(response) if response.status.to_i >= 300 && @retry_on_status.include?(response.status.to_i)
303
+
304
+ rescue OpenSearch::Transport::Transport::ServerError => e
305
+ if response && @retry_on_status.include?(response.status)
306
+ log_warn "[#{e.class}] Attempt #{tries} to get response from #{url}"
307
+ if tries <= (max_retries || DEFAULT_MAX_RETRIES)
308
+ retry
309
+ else
310
+ log_fatal "[#{e.class}] Cannot get response from #{url} after #{tries} tries"
311
+ raise e
312
+ end
313
+ else
314
+ raise e
315
+ end
316
+
317
+ rescue *host_unreachable_exceptions => e
318
+ log_error "[#{e.class}] #{e.message} #{connection.host.inspect}"
319
+
320
+ connection.dead!
321
+
322
+ if reload_on_failure and tries < connections.all.size
323
+ log_warn "[#{e.class}] Reloading connections (attempt #{tries} of #{connections.all.size})"
324
+ reload_connections! and retry
325
+ end
326
+
327
+ if max_retries
328
+ log_warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}"
329
+ if tries <= max_retries
330
+ retry
331
+ else
332
+ log_fatal "[#{e.class}] Cannot connect to #{connection.host.inspect} after #{tries} tries"
333
+ raise e
334
+ end
335
+ else
336
+ raise e
337
+ end
338
+
339
+ rescue Exception => e
340
+ log_fatal "[#{e.class}] #{e.message} (#{connection.host.inspect if connection})"
341
+ raise e
342
+
343
+ end #/begin
344
+
345
+ duration = Time.now - start
346
+
347
+ if response.status.to_i >= 300
348
+ __log_response method, path, params, body, url, response, nil, 'N/A', duration
349
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
350
+
351
+ # Log the failure only when `ignore` doesn't match the response status
352
+ unless ignore.include?(response.status.to_i)
353
+ log_fatal "[#{response.status}] #{response.body}"
354
+ end
355
+
356
+ __raise_transport_error response unless ignore.include?(response.status.to_i)
357
+ end
358
+
359
+ json = serializer.load(response.body) if response.body && !response.body.empty? && response.headers && response.headers["content-type"] =~ /json/
360
+ took = (json['took'] ? sprintf('%.3fs', json['took']/1000.0) : 'n/a') rescue 'n/a'
361
+
362
+ unless ignore.include?(response.status.to_i)
363
+ __log_response method, path, params, body, url, response, json, took, duration
364
+ end
365
+
366
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
367
+
368
+ warnings(response.headers['warning']) if response.headers&.[]('warning')
369
+
370
+ Response.new response.status, json || response.body, response.headers
371
+ ensure
372
+ @last_request_at = Time.now
373
+ end
374
+
375
+ # @abstract Returns an Array of connection errors specific to the transport implementation.
376
+ # See {HTTP::Faraday#host_unreachable_exceptions} for an example.
377
+ #
378
+ # @return [Array]
379
+ #
380
+ def host_unreachable_exceptions
381
+ [Errno::ECONNREFUSED]
382
+ end
383
+
384
+ private
385
+
386
+ USER_AGENT_STR = 'User-Agent'.freeze
387
+ USER_AGENT_REGEX = /user\-?\_?agent/
388
+ CONTENT_TYPE_STR = 'Content-Type'.freeze
389
+ CONTENT_TYPE_REGEX = /content\-?\_?type/
390
+ DEFAULT_CONTENT_TYPE = 'application/json'.freeze
391
+ GZIP = 'gzip'.freeze
392
+ ACCEPT_ENCODING = 'Accept-Encoding'.freeze
393
+ GZIP_FIRST_TWO_BYTES = '1f8b'.freeze
394
+ HEX_STRING_DIRECTIVE = 'H*'.freeze
395
+ RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
396
+
397
+ def decompress_response(body)
398
+ return body unless use_compression?
399
+ return body unless gzipped?(body)
400
+
401
+ io = StringIO.new(body)
402
+ gzip_reader = if RUBY_ENCODING
403
+ Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
404
+ else
405
+ Zlib::GzipReader.new(io)
406
+ end
407
+ gzip_reader.read
408
+ end
409
+
410
+ def gzipped?(body)
411
+ body[0..1].unpack(HEX_STRING_DIRECTIVE)[0] == GZIP_FIRST_TWO_BYTES
412
+ end
413
+
414
+ def use_compression?
415
+ @compression
416
+ end
417
+
418
+ def apply_headers(client, options)
419
+ headers = options[:headers] || {}
420
+ headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
421
+ headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header(client)
422
+ client.headers[ACCEPT_ENCODING] = GZIP if use_compression?
423
+ client.headers.merge!(headers)
424
+ end
425
+
426
+ def find_value(hash, regex)
427
+ key_value = hash.find { |k,v| k.to_s.downcase =~ regex }
428
+ if key_value
429
+ hash.delete(key_value[0])
430
+ key_value[1]
431
+ end
432
+ end
433
+
434
+ def user_agent_header(client)
435
+ @user_agent ||= begin
436
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
437
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
438
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
439
+ end
440
+ "opensearch-ruby/#{VERSION} (#{meta.join('; ')})"
441
+ end
442
+ end
443
+
444
+ def warnings(warning)
445
+ warn("warning: #{warning}")
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,136 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ module OpenSearch
28
+ module Transport
29
+ module Transport
30
+ module Connections
31
+
32
+ # Wraps the collection of connections for the transport object as an Enumerable object.
33
+ #
34
+ # @see Base#connections
35
+ # @see Selector::Base#select
36
+ # @see Connection
37
+ #
38
+ class Collection
39
+ include Enumerable
40
+
41
+ DEFAULT_SELECTOR = Selector::RoundRobin
42
+
43
+ attr_reader :selector
44
+
45
+ # @option arguments [Array] :connections An array of {Connection} objects.
46
+ # @option arguments [Constant] :selector_class The class to be used as a connection selector strategy.
47
+ # @option arguments [Object] :selector The selector strategy object.
48
+ #
49
+ def initialize(arguments={})
50
+ selector_class = arguments[:selector_class] || DEFAULT_SELECTOR
51
+ @connections = arguments[:connections] || []
52
+ @selector = arguments[:selector] || selector_class.new(arguments.merge(:connections => self))
53
+ end
54
+
55
+ # Returns an Array of hosts information in this collection as Hashes.
56
+ #
57
+ # @return [Array]
58
+ #
59
+ def hosts
60
+ @connections.to_a.map { |c| c.host }
61
+ end
62
+
63
+ # Returns an Array of alive connections.
64
+ #
65
+ # @return [Array]
66
+ #
67
+ def connections
68
+ @connections.reject { |c| c.dead? }
69
+ end
70
+ alias :alive :connections
71
+
72
+ # Returns an Array of dead connections.
73
+ #
74
+ # @return [Array]
75
+ #
76
+ def dead
77
+ @connections.select { |c| c.dead? }
78
+ end
79
+
80
+ # Returns an Array of all connections, both dead and alive
81
+ #
82
+ # @return [Array]
83
+ #
84
+ def all
85
+ @connections
86
+ end
87
+
88
+ # Returns a connection.
89
+ #
90
+ # If there are no alive connections, returns a connection with least failures.
91
+ # Delegates to selector's `#select` method to get the connection.
92
+ #
93
+ # @return [Connection]
94
+ #
95
+ def get_connection(options={})
96
+ selector.select(options) || @connections.min_by(&:failures)
97
+ end
98
+
99
+ def each(&block)
100
+ connections.each(&block)
101
+ end
102
+
103
+ def slice(*args)
104
+ connections.slice(*args)
105
+ end
106
+ alias :[] :slice
107
+
108
+ def size
109
+ connections.size
110
+ end
111
+
112
+ # Add connection(s) to the collection
113
+ #
114
+ # @param connections [Connection,Array] A connection or an array of connections to add
115
+ # @return [self]
116
+ #
117
+ def add(connections)
118
+ @connections += Array(connections).to_a
119
+ self
120
+ end
121
+
122
+ # Remove connection(s) from the collection
123
+ #
124
+ # @param connections [Connection,Array] A connection or an array of connections to remove
125
+ # @return [self]
126
+ #
127
+ def remove(connections)
128
+ @connections -= Array(connections).to_a
129
+ self
130
+ end
131
+ end
132
+
133
+ end
134
+ end
135
+ end
136
+ end