elasticsearch-transport 7.1.0 → 7.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14188a454b64314c13f7c9279741fc8df5f9f03ff9813f45aeca96abc8da7e39
4
- data.tar.gz: dfcd7ac5fb8e0b2a0450b2939272f88a329581648c47b130d62e1fcead705b44
3
+ metadata.gz: 47a8f6e85b60bdec378a1c8011aa76c24ab1a08f618a2f2fb38850f3b6114190
4
+ data.tar.gz: 25e88a3e742a0e4341ba685296425d13ddb8ed7059ef97a39174e394bec40535
5
5
  SHA512:
6
- metadata.gz: 36e9583f541ec75994cb27479c16f4624d7ea6a9e6f33360d7cac69541fe10a5c2ee2d7f20b2c1231b5fd07ab85cd32baefb236d6ab77ff11635bb23b1d9d801
7
- data.tar.gz: 95faf1016519c04dcafcd6580356dcfb00c8eeb9ef3301bf2ff3c66a90cfcc8ebf676b638ec18f92f708acb5ef01ee2f7ee137f23ed6532774b371e47b02e11f
6
+ metadata.gz: 90a600694fa1172722d988da7cf09daca12160054b13c1b6a8093303529f62fcd76eb8c4367c0f936fb9d96fdc0533e849ed7c426b04f40069cf009895c06da4
7
+ data.tar.gz: 6f18333cf05353ed2292c7edf21cc320aa8482f72e07bfab104e7ca8b5d0d9d697f26b2562da8455852dd67afe3e1a197edf70133527287574441e85da08b566
data/README.md CHANGED
@@ -106,6 +106,17 @@ Another way to configure the URL(s) is to export the `ELASTICSEARCH_URL` variabl
106
106
  The client will automatically round-robin across the hosts
107
107
  (unless you select or implement a different [connection selector](#connection-selector)).
108
108
 
109
+ ### Connect using an Elastic Cloud ID
110
+
111
+ If you are using [Elastic Cloud](https://www.elastic.co/cloud), you can provide your cloud id to the client.
112
+ You must supply your username and password separately, and optionally a port. If no port is supplied,
113
+ port 9243 will be used.
114
+
115
+ Note: Do not enable sniffing when using Elastic Cloud. The nodes are behind a load balancer so
116
+ Elastic Cloud will take care of everything for you.
117
+
118
+ Elasticsearch::Client.new(cloud_id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', user: 'elastic', password: 'changeme')
119
+
109
120
  ### Authentication
110
121
 
111
122
  You can pass the authentication credentials, scheme and port in the host configuration hash:
@@ -65,6 +65,7 @@ Gem::Specification.new do |s|
65
65
  s.add_development_dependency "patron" unless defined? JRUBY_VERSION
66
66
  s.add_development_dependency "typhoeus", '~> 0.6'
67
67
  s.add_development_dependency "net-http-persistent"
68
+ s.add_development_dependency "httpclient"
68
69
  s.add_development_dependency "manticore", '~> 0.6' if defined? JRUBY_VERSION
69
70
  s.add_development_dependency "hashie"
70
71
 
@@ -15,6 +15,8 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
+ require 'base64'
19
+
18
20
  module Elasticsearch
19
21
  module Transport
20
22
 
@@ -46,6 +48,11 @@ module Elasticsearch
46
48
  # @since 7.0.0
47
49
  DEFAULT_HOST = 'localhost:9200'.freeze
48
50
 
51
+ # The default port to use if connecting using a Cloud ID.
52
+ #
53
+ # @since 7.2.0
54
+ DEFAULT_CLOUD_PORT = 9243
55
+
49
56
  # Returns the transport object.
50
57
  #
51
58
  # @see Elasticsearch::Transport::Transport::Base
@@ -101,6 +108,9 @@ module Elasticsearch
101
108
  #
102
109
  # @option arguments [String] :send_get_body_as Specify the HTTP method to use for GET requests with a body.
103
110
  # (Default: GET)
111
+ # @option arguments [true, false] :compression Whether to compress requests. Gzip compression will be used.
112
+ # The default is false. Responses will automatically be inflated if they are compressed.
113
+ # If a custom transport object is used, it must handle the request compression and response inflation.
104
114
  #
105
115
  # @yield [faraday] Access and configure the `Faraday::Connection` instance directly with a block
106
116
  #
@@ -117,7 +127,8 @@ module Elasticsearch
117
127
  @arguments[:http] ||= {}
118
128
  @options[:http] ||= {}
119
129
 
120
- @seeds = __extract_hosts(@arguments[:hosts] ||
130
+ @seeds = extract_cloud_creds(@arguments)
131
+ @seeds ||= __extract_hosts(@arguments[:hosts] ||
121
132
  @arguments[:host] ||
122
133
  @arguments[:url] ||
123
134
  @arguments[:urls] ||
@@ -130,12 +141,6 @@ module Elasticsearch
130
141
  @arguments[:transport_options][:request] = { :timeout => @arguments[:request_timeout] }
131
142
  end
132
143
 
133
- @arguments[:transport_options][:headers] ||= {}
134
-
135
- unless @arguments[:transport_options][:headers].keys.any? {|k| k.to_s.downcase =~ /content\-?\_?type/}
136
- @arguments[:transport_options][:headers]['Content-Type'] = 'application/json'
137
- end
138
-
139
144
  if @arguments[:transport]
140
145
  @transport = @arguments[:transport]
141
146
  else
@@ -162,6 +167,16 @@ module Elasticsearch
162
167
 
163
168
  private
164
169
 
170
+ def extract_cloud_creds(arguments)
171
+ return unless arguments[:cloud_id]
172
+ cloud_url, elasticsearch_instance = Base64.decode64(arguments[:cloud_id].gsub('name:', '')).split('$')
173
+ [ { scheme: 'https',
174
+ user: arguments[:user],
175
+ password: arguments[:password],
176
+ host: "#{elasticsearch_instance}.#{cloud_url}",
177
+ port: arguments[:port] || DEFAULT_CLOUD_PORT } ]
178
+ end
179
+
165
180
  # Normalizes and returns hosts configuration.
166
181
  #
167
182
  # Arrayifies the `hosts_config` argument and extracts `host` and `port` info from strings.
@@ -56,6 +56,7 @@ module Elasticsearch
56
56
  @options[:retry_on_status] ||= []
57
57
 
58
58
  @block = block
59
+ @compression = !!@options[:compression]
59
60
  @connections = __build_connections
60
61
 
61
62
  @serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self) )
@@ -202,7 +203,7 @@ module Elasticsearch
202
203
  ( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
203
204
  trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
204
205
  trace_command = "curl -X #{method.to_s.upcase}"
205
- trace_command += " -H '#{headers.inject('') { |memo,item| memo << item[0] + ': ' + item[1] }}'" if headers && !headers.empty?
206
+ trace_command += " -H '#{headers.collect { |k,v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
206
207
  trace_command += " '#{trace_url}'#{trace_body}\n"
207
208
  tracer.info trace_command
208
209
  tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
@@ -234,7 +235,8 @@ module Elasticsearch
234
235
  def __full_url(host)
235
236
  url = "#{host[:protocol]}://"
236
237
  url += "#{CGI.escape(host[:user])}:#{CGI.escape(host[:password])}@" if host[:user]
237
- url += "#{host[:host]}:#{host[:port]}"
238
+ url += "#{host[:host]}"
239
+ url += ":#{host[:port]}" if host[:port]
238
240
  url += "#{host[:path]}" if host[:path]
239
241
  url
240
242
  end
@@ -328,7 +330,7 @@ module Elasticsearch
328
330
 
329
331
  if response.status.to_i >= 300
330
332
  __log_response method, path, params, body, url, response, nil, 'N/A', duration
331
- __trace method, path, params, headers, body, url, response, nil, 'N/A', duration if tracer
333
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
332
334
 
333
335
  # Log the failure only when `ignore` doesn't match the response status
334
336
  unless ignore.include?(response.status.to_i)
@@ -345,7 +347,7 @@ module Elasticsearch
345
347
  __log_response method, path, params, body, url, response, json, took, duration
346
348
  end
347
349
 
348
- __trace method, path, params, headers, body, url, response, json, took, duration if tracer
350
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
349
351
 
350
352
  Response.new response.status, json || response.body, response.headers
351
353
  ensure
@@ -360,6 +362,66 @@ module Elasticsearch
360
362
  def host_unreachable_exceptions
361
363
  [Errno::ECONNREFUSED]
362
364
  end
365
+
366
+ private
367
+
368
+ USER_AGENT_STR = 'User-Agent'.freeze
369
+ USER_AGENT_REGEX = /user\-?\_?agent/
370
+ CONTENT_TYPE_STR = 'Content-Type'.freeze
371
+ CONTENT_TYPE_REGEX = /content\-?\_?type/
372
+ DEFAULT_CONTENT_TYPE = 'application/json'.freeze
373
+ GZIP = 'gzip'.freeze
374
+ ACCEPT_ENCODING = 'Accept-Encoding'.freeze
375
+ GZIP_FIRST_TWO_BYTES = '1f8b'.freeze
376
+ HEX_STRING_DIRECTIVE = 'H*'.freeze
377
+ RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
378
+
379
+ def decompress_response(body)
380
+ return body unless use_compression?
381
+ return body unless gzipped?(body)
382
+
383
+ io = StringIO.new(body)
384
+ gzip_reader = if RUBY_ENCODING
385
+ Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
386
+ else
387
+ Zlib::GzipReader.new(io)
388
+ end
389
+ gzip_reader.read
390
+ end
391
+
392
+ def gzipped?(body)
393
+ body[0..1].unpack(HEX_STRING_DIRECTIVE)[0] == GZIP_FIRST_TWO_BYTES
394
+ end
395
+
396
+ def use_compression?
397
+ @compression
398
+ end
399
+
400
+ def apply_headers(client, options)
401
+ headers = options[:headers] || {}
402
+ headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
403
+ headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header(client)
404
+ client.headers[ACCEPT_ENCODING] = GZIP if use_compression?
405
+ client.headers.merge!(headers)
406
+ end
407
+
408
+ def find_value(hash, regex)
409
+ key_value = hash.find { |k,v| k.to_s.downcase =~ regex }
410
+ if key_value
411
+ hash.delete(key_value[0])
412
+ key_value[1]
413
+ end
414
+ end
415
+
416
+ def user_agent_header(client)
417
+ @user_agent ||= begin
418
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
419
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
420
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
421
+ end
422
+ "elasticsearch-ruby/#{VERSION} (#{meta.join('; ')})"
423
+ end
424
+ end
363
425
  end
364
426
  end
365
427
  end
@@ -61,18 +61,30 @@ module Elasticsearch
61
61
  class RoundRobin
62
62
  include Base
63
63
 
64
+ # @option arguments [Connections::Collection] :connections Collection with connections.
65
+ #
66
+ def initialize(arguments = {})
67
+ super
68
+ @mutex = Mutex.new
69
+ @current = nil
70
+ end
71
+
64
72
  # Returns the next connection from the collection, rotating them in round-robin fashion.
65
73
  #
66
74
  # @return [Connections::Connection]
67
75
  #
68
76
  def select(options={})
69
- # On Ruby 1.9, Array#rotate could be used instead
70
- @current = !defined?(@current) || @current.nil? ? 0 : @current+1
71
- @current = 0 if @current >= connections.size
72
- connections[@current]
77
+ @mutex.synchronize do
78
+ conns = connections
79
+ if @current && (@current < conns.size-1)
80
+ @current += 1
81
+ else
82
+ @current = 0
83
+ end
84
+ conns[@current]
85
+ end
73
86
  end
74
87
  end
75
-
76
88
  end
77
89
  end
78
90
  end
@@ -43,7 +43,15 @@ module Elasticsearch
43
43
  connection.connection.set :nobody, false
44
44
 
45
45
  connection.connection.put_data = __convert_to_json(body) if body
46
- connection.connection.headers = headers if headers
46
+
47
+ if headers
48
+ if connection.connection.headers
49
+ connection.connection.headers.merge!(headers)
50
+ else
51
+ connection.connection.headers = headers
52
+ end
53
+ end
54
+
47
55
  else raise ArgumentError, "Unsupported HTTP method: #{method}"
48
56
  end
49
57
 
@@ -53,7 +61,7 @@ module Elasticsearch
53
61
  response_headers['content-type'] = 'application/json' if connection.connection.header_str =~ /\/json/
54
62
 
55
63
  Response.new connection.connection.response_code,
56
- connection.connection.body_str,
64
+ decompress_response(connection.connection.body_str),
57
65
  response_headers
58
66
  end
59
67
  end
@@ -65,10 +73,7 @@ module Elasticsearch
65
73
  def __build_connection(host, options={}, block=nil)
66
74
  client = ::Curl::Easy.new
67
75
 
68
- headers = options[:headers] || {}
69
- headers.update('User-Agent' => "Curb #{Curl::CURB_VERSION}")
70
-
71
- client.headers = headers
76
+ apply_headers(client, options)
72
77
  client.url = __full_url(host)
73
78
 
74
79
  if host[:user]
@@ -96,8 +101,20 @@ module Elasticsearch
96
101
  ::Curl::Err::TimeoutError
97
102
  ]
98
103
  end
99
- end
100
104
 
105
+ private
106
+
107
+ def user_agent_header(client)
108
+ @user_agent ||= begin
109
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
110
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
111
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
112
+ end
113
+ meta << "Curb #{Curl::CURB_VERSION}"
114
+ "elasticsearch-ruby/#{VERSION} (#{meta.join('; ')})"
115
+ end
116
+ end
117
+ end
101
118
  end
102
119
  end
103
120
  end
@@ -42,7 +42,7 @@ module Elasticsearch
42
42
  ( body ? __convert_to_json(body) : nil ),
43
43
  headers)
44
44
 
45
- Response.new response.status, response.body, response.headers
45
+ Response.new response.status, decompress_response(response.body), response.headers
46
46
  end
47
47
  end
48
48
 
@@ -52,6 +52,7 @@ module Elasticsearch
52
52
  #
53
53
  def __build_connection(host, options={}, block=nil)
54
54
  client = ::Faraday.new(__full_url(host), options, &block)
55
+ apply_headers(client, options)
55
56
  Connections::Connection.new :host => host, :connection => client
56
57
  end
57
58
 
@@ -62,6 +63,19 @@ module Elasticsearch
62
63
  def host_unreachable_exceptions
63
64
  [::Faraday::Error::ConnectionFailed, ::Faraday::Error::TimeoutError]
64
65
  end
66
+
67
+ private
68
+
69
+ def user_agent_header(client)
70
+ @user_agent ||= begin
71
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
72
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
73
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
74
+ end
75
+ meta << "#{client.headers[USER_AGENT_STR]}"
76
+ "elasticsearch-ruby/#{VERSION} (#{meta.join('; ')})"
77
+ end
78
+ end
65
79
  end
66
80
  end
67
81
  end
@@ -110,14 +110,8 @@ module Elasticsearch
110
110
  #
111
111
  def __build_connections
112
112
  @request_options = {}
113
-
114
- if options[:transport_options] && options[:transport_options][:headers]
115
- @request_options[:headers] = options[:transport_options][:headers]
116
- end
117
-
118
- if options.key?(:headers)
119
- @request_options[:headers] = options[:headers]
120
- end
113
+ apply_headers(@request_options, options[:transport_options])
114
+ apply_headers(@request_options, options)
121
115
 
122
116
  Connections::Collection.new \
123
117
  :connections => hosts.map { |host|
@@ -157,6 +151,27 @@ module Elasticsearch
157
151
  ::Manticore::ResolutionFailure
158
152
  ]
159
153
  end
154
+
155
+ private
156
+
157
+ def apply_headers(request_options, options)
158
+ headers = (options && options[:headers]) || {}
159
+ headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
160
+ headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header
161
+ headers[ACCEPT_ENCODING] = GZIP if use_compression?
162
+ request_options.merge!(headers: headers)
163
+ end
164
+
165
+ def user_agent_header
166
+ @user_agent ||= begin
167
+ meta = ["RUBY_VERSION: #{JRUBY_VERSION}"]
168
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
169
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
170
+ end
171
+ meta << "Manticore #{::Manticore::VERSION}"
172
+ "elasticsearch-ruby/#{VERSION} (#{meta.join('; ')})"
173
+ end
174
+ end
160
175
  end
161
176
  end
162
177
  end
@@ -17,6 +17,6 @@
17
17
 
18
18
  module Elasticsearch
19
19
  module Transport
20
- VERSION = "7.1.0"
20
+ VERSION = "7.2.0"
21
21
  end
22
22
  end
@@ -0,0 +1,254 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # 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,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ require 'spec_helper'
19
+
20
+ describe Elasticsearch::Transport::Transport::Connections::Collection do
21
+
22
+ describe '#initialize' do
23
+
24
+ let(:collection) do
25
+ described_class.new
26
+ end
27
+
28
+ it 'has an empty list of connections as a default' do
29
+ expect(collection.connections).to be_empty
30
+ end
31
+
32
+ it 'has a default selector class' do
33
+ expect(collection.selector).not_to be_nil
34
+ end
35
+
36
+ context 'when a selector class is specified' do
37
+
38
+ let(:collection) do
39
+ described_class.new(selector_class: Elasticsearch::Transport::Transport::Connections::Selector::Random)
40
+ end
41
+
42
+ it 'sets the selector' do
43
+ expect(collection.selector).to be_a(Elasticsearch::Transport::Transport::Connections::Selector::Random)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#get_connection' do
49
+
50
+ let(:collection) do
51
+ described_class.new(selector_class: Elasticsearch::Transport::Transport::Connections::Selector::Random)
52
+ end
53
+
54
+ before do
55
+ expect(collection.selector).to receive(:select).and_return('OK')
56
+ end
57
+
58
+ it 'uses the selector to select a connection' do
59
+ expect(collection.get_connection).to eq('OK')
60
+ end
61
+ end
62
+
63
+ describe '#hosts' do
64
+
65
+ let(:collection) do
66
+ described_class.new(connections: [ double('connection', host: 'A'),
67
+ double('connection', host: 'B') ])
68
+ end
69
+
70
+ it 'returns a list of hosts' do
71
+ expect(collection.hosts).to eq([ 'A', 'B'])
72
+ end
73
+ end
74
+
75
+ describe 'enumerable' do
76
+
77
+ let(:collection) do
78
+ described_class.new(connections: [ double('connection', host: 'A', dead?: false),
79
+ double('connection', host: 'B', dead?: false) ])
80
+ end
81
+
82
+ describe '#map' do
83
+
84
+ it 'responds to the method' do
85
+ expect(collection.map { |c| c.host.downcase }).to eq(['a', 'b'])
86
+ end
87
+ end
88
+
89
+ describe '#[]' do
90
+
91
+ it 'responds to the method' do
92
+ expect(collection[0].host).to eq('A')
93
+ expect(collection[1].host).to eq('B')
94
+ end
95
+ end
96
+
97
+ describe '#size' do
98
+
99
+ it 'responds to the method' do
100
+ expect(collection.size).to eq(2)
101
+ end
102
+ end
103
+
104
+ context 'when a connection is marked as dead' do
105
+
106
+ let(:collection) do
107
+ described_class.new(connections: [ double('connection', host: 'A', dead?: true),
108
+ double('connection', host: 'B', dead?: false) ])
109
+ end
110
+
111
+ it 'does not enumerate the dead connections' do
112
+ expect(collection.size).to eq(1)
113
+ expect(collection.collect { |c| c.host }).to eq(['B'])
114
+ end
115
+
116
+ context '#alive' do
117
+
118
+ it 'enumerates the alive connections' do
119
+ expect(collection.alive.collect { |c| c.host }).to eq(['B'])
120
+ end
121
+ end
122
+
123
+ context '#dead' do
124
+
125
+ it 'enumerates the alive connections' do
126
+ expect(collection.dead.collect { |c| c.host }).to eq(['A'])
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ describe '#add' do
133
+
134
+ let(:collection) do
135
+ described_class.new(connections: [ double('connection', host: 'A', dead?: false),
136
+ double('connection', host: 'B', dead?: false) ])
137
+ end
138
+
139
+ context 'when an array is provided' do
140
+
141
+ before do
142
+ collection.add([double('connection', host: 'C', dead?: false),
143
+ double('connection', host: 'D', dead?: false)])
144
+ end
145
+
146
+ it 'adds the connections' do
147
+ expect(collection.size).to eq(4)
148
+ end
149
+ end
150
+
151
+ context 'when an element is provided' do
152
+
153
+ before do
154
+ collection.add(double('connection', host: 'C', dead?: false))
155
+ end
156
+
157
+ it 'adds the connection' do
158
+ expect(collection.size).to eq(3)
159
+ end
160
+ end
161
+ end
162
+
163
+ describe '#remove' do
164
+
165
+ let(:connections) do
166
+ [ double('connection', host: 'A', dead?: false),
167
+ double('connection', host: 'B', dead?: false) ]
168
+ end
169
+
170
+ let(:collection) do
171
+ described_class.new(connections: connections)
172
+ end
173
+
174
+ context 'when an array is provided' do
175
+
176
+ before do
177
+ collection.remove(connections)
178
+ end
179
+
180
+ it 'removes the connections' do
181
+ expect(collection.size).to eq(0)
182
+ end
183
+ end
184
+
185
+ context 'when an element is provided' do
186
+
187
+ let(:connections) do
188
+ [ double('connection', host: 'A', dead?: false),
189
+ double('connection', host: 'B', dead?: false) ]
190
+ end
191
+
192
+ before do
193
+ collection.remove(connections.first)
194
+ end
195
+
196
+ it 'removes the connection' do
197
+ expect(collection.size).to eq(1)
198
+ end
199
+ end
200
+ end
201
+
202
+ describe '#get_connection' do
203
+
204
+ context 'when all connections are dead' do
205
+
206
+ let(:connection_a) do
207
+ Elasticsearch::Transport::Transport::Connections::Connection.new(host: { host: 'A' })
208
+ end
209
+
210
+ let(:connection_b) do
211
+ Elasticsearch::Transport::Transport::Connections::Connection.new(host: { host: 'B' })
212
+ end
213
+
214
+ let(:collection) do
215
+ described_class.new(connections: [connection_a, connection_b])
216
+ end
217
+
218
+ before do
219
+ connection_a.dead!.dead!
220
+ connection_b.dead!
221
+ end
222
+
223
+ it 'returns the connection with the least failures' do
224
+ expect(collection.get_connection.host[:host]).to eq('B')
225
+ end
226
+ end
227
+
228
+ context 'when multiple threads are used' do
229
+
230
+ let(:connections) do
231
+ 20.times.collect do |i|
232
+ Elasticsearch::Transport::Transport::Connections::Connection.new(host: { host: i })
233
+ end
234
+ end
235
+
236
+ let(:collection) do
237
+ described_class.new(connections: connections)
238
+ end
239
+
240
+ it 'allows threads to select connections in parallel' do
241
+ expect(10.times.collect do
242
+ threads = []
243
+ 20.times do
244
+ threads << Thread.new do
245
+ collection.get_connection
246
+ end
247
+ end
248
+ threads.map { |t| t.join }
249
+ collection.get_connection.host[:host]
250
+ end).to eq((0..9).to_a)
251
+ end
252
+ end
253
+ end
254
+ end