opensearch-transport 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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