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 +4 -4
- data/README.md +11 -0
- data/elasticsearch-transport.gemspec +1 -0
- data/lib/elasticsearch/transport/client.rb +22 -7
- data/lib/elasticsearch/transport/transport/base.rb +66 -4
- data/lib/elasticsearch/transport/transport/connections/selector.rb +17 -5
- data/lib/elasticsearch/transport/transport/http/curb.rb +24 -7
- data/lib/elasticsearch/transport/transport/http/faraday.rb +15 -1
- data/lib/elasticsearch/transport/transport/http/manticore.rb +23 -8
- data/lib/elasticsearch/transport/version.rb +1 -1
- data/spec/elasticsearch/connections/collection_spec.rb +254 -0
- data/spec/elasticsearch/connections/selector_spec.rb +174 -0
- data/spec/elasticsearch/transport/client_spec.rb +323 -3
- data/spec/spec_helper.rb +6 -0
- data/test/unit/transport_curb_test.rb +1 -1
- data/test/unit/transport_manticore_test.rb +27 -11
- metadata +20 -6
- data/test/unit/connection_collection_test.rb +0 -147
- data/test/unit/connection_selector_test.rb +0 -81
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47a8f6e85b60bdec378a1c8011aa76c24ab1a08f618a2f2fb38850f3b6114190
|
4
|
+
data.tar.gz: 25e88a3e742a0e4341ba685296425d13ddb8ed7059ef97a39174e394bec40535
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 =
|
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.
|
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]}
|
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
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|