elasticsearch-transport 7.1.0 → 7.2.0

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