elasticsearch-transport 7.1.0 → 7.13.3

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 +4 -4
  2. data/Gemfile +13 -9
  3. data/{LICENSE.txt → LICENSE} +0 -0
  4. data/README.md +175 -76
  5. data/Rakefile +1 -1
  6. data/elasticsearch-transport.gemspec +42 -60
  7. data/lib/elasticsearch/transport/client.rb +154 -57
  8. data/lib/elasticsearch/transport/meta_header.rb +135 -0
  9. data/lib/elasticsearch/transport/redacted.rb +1 -1
  10. data/lib/elasticsearch/transport/transport/base.rb +93 -18
  11. data/lib/elasticsearch/transport/transport/connections/collection.rb +3 -6
  12. data/lib/elasticsearch/transport/transport/connections/connection.rb +8 -6
  13. data/lib/elasticsearch/transport/transport/connections/selector.rb +18 -6
  14. data/lib/elasticsearch/transport/transport/errors.rb +1 -1
  15. data/lib/elasticsearch/transport/transport/http/curb.rb +26 -9
  16. data/lib/elasticsearch/transport/transport/http/faraday.rb +27 -5
  17. data/lib/elasticsearch/transport/transport/http/manticore.rb +25 -10
  18. data/lib/elasticsearch/transport/transport/loggable.rb +1 -1
  19. data/lib/elasticsearch/transport/transport/response.rb +1 -2
  20. data/lib/elasticsearch/transport/transport/serializer/multi_json.rb +1 -1
  21. data/lib/elasticsearch/transport/transport/sniffer.rb +20 -12
  22. data/lib/elasticsearch/transport/version.rb +2 -2
  23. data/lib/elasticsearch/transport.rb +1 -1
  24. data/lib/elasticsearch-transport.rb +1 -1
  25. data/spec/elasticsearch/connections/collection_spec.rb +266 -0
  26. data/spec/elasticsearch/connections/selector_spec.rb +174 -0
  27. data/spec/elasticsearch/transport/base_spec.rb +197 -13
  28. data/spec/elasticsearch/transport/client_spec.rb +945 -118
  29. data/spec/elasticsearch/transport/meta_header_spec.rb +265 -0
  30. data/spec/elasticsearch/transport/sniffer_spec.rb +1 -14
  31. data/spec/spec_helper.rb +25 -1
  32. data/test/integration/transport_test.rb +15 -2
  33. data/test/profile/client_benchmark_test.rb +1 -1
  34. data/test/test_helper.rb +1 -1
  35. data/test/unit/connection_test.rb +8 -3
  36. data/test/unit/response_test.rb +2 -2
  37. data/test/unit/serializer_test.rb +1 -1
  38. data/test/unit/transport_base_test.rb +2 -2
  39. data/test/unit/transport_curb_test.rb +2 -2
  40. data/test/unit/transport_faraday_test.rb +3 -3
  41. data/test/unit/transport_manticore_test.rb +30 -14
  42. metadata +87 -60
  43. data/test/unit/connection_collection_test.rb +0 -147
  44. data/test/unit/connection_selector_test.rb +0 -81
@@ -6,7 +6,7 @@
6
6
  # not use this file except in compliance with the License.
7
7
  # You may obtain a copy of the License at
8
8
  #
9
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
10
  #
11
11
  # Unless required by applicable law or agreed to in writing,
12
12
  # software distributed under the License is distributed on an
@@ -15,6 +15,9 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
+ require 'base64'
19
+ require 'elasticsearch/transport/meta_header'
20
+
18
21
  module Elasticsearch
19
22
  module Transport
20
23
 
@@ -23,6 +26,7 @@ module Elasticsearch
23
26
  # See {file:README.md README} for usage and code examples.
24
27
  #
25
28
  class Client
29
+ include MetaHeader
26
30
  DEFAULT_TRANSPORT_CLASS = Transport::HTTP::Faraday
27
31
 
28
32
  DEFAULT_LOGGER = lambda do
@@ -46,6 +50,17 @@ module Elasticsearch
46
50
  # @since 7.0.0
47
51
  DEFAULT_HOST = 'localhost:9200'.freeze
48
52
 
53
+ # The default port to use if connecting using a Cloud ID.
54
+ # Updated from 9243 to 443 in client version 7.10.1
55
+ #
56
+ # @since 7.2.0
57
+ DEFAULT_CLOUD_PORT = 443
58
+
59
+ # The default port to use if not otherwise specified.
60
+ #
61
+ # @since 7.2.0
62
+ DEFAULT_PORT = 9200
63
+
49
64
  # Returns the transport object.
50
65
  #
51
66
  # @see Elasticsearch::Transport::Transport::Base
@@ -101,6 +116,17 @@ module Elasticsearch
101
116
  #
102
117
  # @option arguments [String] :send_get_body_as Specify the HTTP method to use for GET requests with a body.
103
118
  # (Default: GET)
119
+ # @option arguments [true, false] :compression Whether to compress requests. Gzip compression will be used.
120
+ # The default is false. Responses will automatically be inflated if they are compressed.
121
+ # If a custom transport object is used, it must handle the request compression and response inflation.
122
+ #
123
+ # @option api_key [String, Hash] :api_key Use API Key Authentication, either the base64 encoding of `id` and `api_key`
124
+ # joined by a colon as a String, or a hash with the `id` and `api_key` values.
125
+ # @option opaque_id_prefix [String] :opaque_id_prefix set a prefix for X-Opaque-Id when initializing the client.
126
+ # This will be prepended to the id you set before each request
127
+ # if you're using X-Opaque-Id
128
+ # @option enable_meta_header [Boolean] :enable_meta_header Enable sending the meta data header to Cloud.
129
+ # (Default: true)
104
130
  #
105
131
  # @yield [faraday] Access and configure the `Faraday::Connection` instance directly with a block
106
132
  #
@@ -115,53 +141,109 @@ module Elasticsearch
115
141
  @arguments[:randomize_hosts] ||= false
116
142
  @arguments[:transport_options] ||= {}
117
143
  @arguments[:http] ||= {}
118
- @options[:http] ||= {}
144
+ @arguments[:enable_meta_header] = arguments.fetch(:enable_meta_header) { true }
145
+ @options[:http] ||= {}
146
+
147
+ set_api_key if (@api_key = @arguments[:api_key])
148
+ set_compatibility_header if ENV['ELASTIC_CLIENT_APIVERSIONING']
119
149
 
120
- @seeds = __extract_hosts(@arguments[:hosts] ||
121
- @arguments[:host] ||
122
- @arguments[:url] ||
123
- @arguments[:urls] ||
124
- ENV['ELASTICSEARCH_URL'] ||
125
- DEFAULT_HOST)
150
+ @seeds = extract_cloud_creds(@arguments)
151
+ @seeds ||= __extract_hosts(@arguments[:hosts] ||
152
+ @arguments[:host] ||
153
+ @arguments[:url] ||
154
+ @arguments[:urls] ||
155
+ ENV['ELASTICSEARCH_URL'] ||
156
+ DEFAULT_HOST)
126
157
 
127
158
  @send_get_body_as = @arguments[:send_get_body_as] || 'GET'
159
+ @opaque_id_prefix = @arguments[:opaque_id_prefix] || nil
128
160
 
129
161
  if @arguments[:request_timeout]
130
- @arguments[:transport_options][:request] = { :timeout => @arguments[:request_timeout] }
131
- end
132
-
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'
162
+ @arguments[:transport_options][:request] = { timeout: @arguments[:request_timeout] }
137
163
  end
138
164
 
139
165
  if @arguments[:transport]
140
166
  @transport = @arguments[:transport]
141
167
  else
142
- transport_class = @arguments[:transport_class] || DEFAULT_TRANSPORT_CLASS
143
- if transport_class == Transport::HTTP::Faraday
144
- @transport = transport_class.new(:hosts => @seeds, :options => @arguments) do |faraday|
145
- block.call faraday if block
146
- unless (h = faraday.builder.handlers.last) && h.name.start_with?("Faraday::Adapter")
147
- faraday.adapter(@arguments[:adapter] || __auto_detect_adapter)
148
- end
149
- end
150
- else
151
- @transport = transport_class.new(:hosts => @seeds, :options => @arguments)
152
- end
168
+ @transport_class = @arguments[:transport_class] || DEFAULT_TRANSPORT_CLASS
169
+ @transport = if @transport_class == Transport::HTTP::Faraday
170
+ @arguments[:adapter] ||= __auto_detect_adapter
171
+ set_meta_header # from include MetaHeader
172
+ @transport_class.new(hosts: @seeds, options: @arguments) do |faraday|
173
+ faraday.adapter(@arguments[:adapter])
174
+ block&.call faraday
175
+ end
176
+ else
177
+ set_meta_header # from include MetaHeader
178
+ @transport_class.new(hosts: @seeds, options: @arguments)
179
+ end
153
180
  end
154
181
  end
155
182
 
156
183
  # Performs a request through delegation to {#transport}.
157
184
  #
158
- def perform_request(method, path, params={}, body=nil, headers=nil)
185
+ def perform_request(method, path, params = {}, body = nil, headers = nil)
159
186
  method = @send_get_body_as if 'GET' == method && body
187
+ if (opaque_id = params.delete(:opaque_id))
188
+ headers = {} if headers.nil?
189
+ opaque_id = @opaque_id_prefix ? "#{@opaque_id_prefix}#{opaque_id}" : opaque_id
190
+ headers.merge!('X-Opaque-Id' => opaque_id)
191
+ end
160
192
  transport.perform_request(method, path, params, body, headers)
161
193
  end
162
194
 
163
195
  private
164
196
 
197
+ def set_api_key
198
+ @api_key = __encode(@api_key) if @api_key.is_a? Hash
199
+ add_header('Authorization' => "ApiKey #{@api_key}")
200
+ @arguments.delete(:user)
201
+ @arguments.delete(:password)
202
+ end
203
+
204
+ def set_compatibility_header
205
+ return unless ['1', 'true'].include?(ENV['ELASTIC_CLIENT_APIVERSIONING'])
206
+
207
+ add_header(
208
+ {
209
+ 'Accept' => 'application/vnd.elasticsearch+json;compatible-with=7',
210
+ 'Content-Type' => 'application/vnd.elasticsearch+json; compatible-with=7'
211
+ }
212
+ )
213
+ end
214
+
215
+ def add_header(header)
216
+ headers = @arguments[:transport_options]&.[](:headers) || {}
217
+ headers.merge!(header)
218
+ @arguments[:transport_options].merge!(
219
+ headers: headers
220
+ )
221
+ end
222
+
223
+ def extract_cloud_creds(arguments)
224
+ return unless arguments[:cloud_id] && !arguments[:cloud_id].empty?
225
+
226
+ name = arguments[:cloud_id].split(':')[0]
227
+ cloud_url, elasticsearch_instance = Base64.decode64(arguments[:cloud_id].gsub("#{name}:", '')).split('$')
228
+
229
+ if cloud_url.include?(':')
230
+ url, port = cloud_url.split(':')
231
+ host = "#{elasticsearch_instance}.#{url}"
232
+ else
233
+ host = "#{elasticsearch_instance}.#{cloud_url}"
234
+ port = arguments[:port] || DEFAULT_CLOUD_PORT
235
+ end
236
+ [
237
+ {
238
+ scheme: 'https',
239
+ user: arguments[:user],
240
+ password: arguments[:password],
241
+ host: host,
242
+ port: port.to_i
243
+ }
244
+ ]
245
+ end
246
+
165
247
  # Normalizes and returns hosts configuration.
166
248
  #
167
249
  # Arrayifies the `hosts_config` argument and extracts `host` and `port` info from strings.
@@ -192,39 +274,47 @@ module Elasticsearch
192
274
 
193
275
  def __parse_host(host)
194
276
  host_parts = case host
195
- when String
196
- if host =~ /^[a-z]+\:\/\//
197
- # Construct a new `URI::Generic` directly from the array returned by URI::split.
198
- # This avoids `URI::HTTP` and `URI::HTTPS`, which supply default ports.
199
- uri = URI::Generic.new(*URI.split(host))
200
-
201
- { :scheme => uri.scheme,
202
- :user => uri.user,
203
- :password => uri.password,
204
- :host => uri.host,
205
- :path => uri.path,
206
- :port => uri.port }
207
- else
208
- host, port = host.split(':')
209
- { :host => host,
210
- :port => port }
211
- end
212
- when URI
213
- { :scheme => host.scheme,
214
- :user => host.user,
215
- :password => host.password,
216
- :host => host.host,
217
- :path => host.path,
218
- :port => host.port }
219
- when Hash
220
- host
277
+ when String
278
+ if host =~ /^[a-z]+\:\/\//
279
+ # Construct a new `URI::Generic` directly from the array returned by URI::split.
280
+ # This avoids `URI::HTTP` and `URI::HTTPS`, which supply default ports.
281
+ uri = URI::Generic.new(*URI.split(host))
282
+ default_port = uri.scheme == 'https' ? 443 : DEFAULT_PORT
283
+ {
284
+ scheme: uri.scheme,
285
+ user: uri.user,
286
+ password: uri.password,
287
+ host: uri.host,
288
+ path: uri.path,
289
+ port: uri.port || default_port
290
+ }
291
+ else
292
+ host, port = host.split(':')
293
+ { host: host, port: port }
294
+ end
295
+ when URI
296
+ {
297
+ scheme: host.scheme,
298
+ user: host.user,
299
+ password: host.password,
300
+ host: host.host,
301
+ path: host.path,
302
+ port: host.port
303
+ }
304
+ when Hash
305
+ host
306
+ else
307
+ raise ArgumentError, "Please pass host as a String, URI or Hash -- #{host.class} given."
308
+ end
309
+ if @api_key
310
+ # Remove Basic Auth if using API KEY
311
+ host_parts.delete(:user)
312
+ host_parts.delete(:password)
221
313
  else
222
- raise ArgumentError, "Please pass host as a String, URI or Hash -- #{host.class} given."
314
+ @options[:http][:user] ||= host_parts[:user]
315
+ @options[:http][:password] ||= host_parts[:password]
223
316
  end
224
317
 
225
- @options[:http][:user] ||= host_parts[:user]
226
- @options[:http][:password] ||= host_parts[:password]
227
-
228
318
  host_parts[:port] = host_parts[:port].to_i if host_parts[:port]
229
319
  host_parts[:path].chomp!('/') if host_parts[:path]
230
320
  host_parts
@@ -252,6 +342,13 @@ module Elasticsearch
252
342
  ::Faraday.default_adapter
253
343
  end
254
344
  end
345
+
346
+ # Encode credentials for the Authorization Header
347
+ # Credentials is the base64 encoding of id and api_key joined by a colon
348
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
349
+ def __encode(api_key)
350
+ Base64.strict_encode64([api_key[:id], api_key[:api_key]].join(':'))
351
+ end
255
352
  end
256
353
  end
257
354
  end
@@ -0,0 +1,135 @@
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 'base64'
19
+
20
+ module Elasticsearch
21
+ module Transport
22
+ # Methods for the Elastic meta header used by Cloud.
23
+ # X-Elastic-Client-Meta HTTP header which is used by Elastic Cloud and can be disabled when
24
+ # instantiating the Client with the :enable_meta_header parameter set to `false`.
25
+ #
26
+ module MetaHeader
27
+ def set_meta_header
28
+ return if @arguments[:enable_meta_header] == false
29
+
30
+ service, version = meta_header_service_version
31
+
32
+ meta_headers = {
33
+ service.to_sym => version,
34
+ rb: RUBY_VERSION,
35
+ t: Elasticsearch::Transport::VERSION
36
+ }
37
+ meta_headers.merge!(meta_header_engine) if meta_header_engine
38
+ meta_headers.merge!(meta_header_adapter) if meta_header_adapter
39
+
40
+ add_header({ 'x-elastic-client-meta' => meta_headers.map { |k, v| "#{k}=#{v}" }.join(',') })
41
+ end
42
+
43
+ def meta_header_service_version
44
+ if enterprise_search?
45
+ Elastic::ENTERPRISE_SERVICE_VERSION
46
+ elsif elasticsearch?
47
+ Elastic::ELASTICSEARCH_SERVICE_VERSION
48
+ elsif defined?(Elasticsearch::VERSION)
49
+ [:es, client_meta_version(Elasticsearch::VERSION)]
50
+ else
51
+ [:es, client_meta_version(Elasticsearch::Transport::VERSION)]
52
+ end
53
+ end
54
+
55
+ def enterprise_search?
56
+ defined?(Elastic::ENTERPRISE_SERVICE_VERSION) &&
57
+ called_from?('enterprise-search-ruby')
58
+ end
59
+
60
+ def elasticsearch?
61
+ defined?(Elastic::ELASTICSEARCH_SERVICE_VERSION) &&
62
+ called_from?('elasticsearch')
63
+ end
64
+
65
+ def called_from?(service)
66
+ !caller.select { |c| c.match?(service) }.empty?
67
+ end
68
+
69
+ # We return the current version if it's a release, but if it's a pre/alpha/beta release we
70
+ # return <VERSION_NUMBER>p
71
+ #
72
+ def client_meta_version(version)
73
+ regexp = /^([0-9]+\.[0-9]+\.[0-9]+)(\.?[a-z0-9.-]+)?$/
74
+ match = version.match(regexp)
75
+ return "#{match[1]}p" if (match[2])
76
+
77
+ version
78
+ end
79
+
80
+ def meta_header_engine
81
+ case RUBY_ENGINE
82
+ when 'ruby'
83
+ {}
84
+ when 'jruby'
85
+ { jv: ENV_JAVA['java.version'], jr: JRUBY_VERSION }
86
+ when 'rbx'
87
+ { rbx: RUBY_VERSION }
88
+ else
89
+ { RUBY_ENGINE.to_sym => RUBY_VERSION }
90
+ end
91
+ end
92
+
93
+ # This function tries to define the version for the Faraday adapter. If it hasn't been loaded
94
+ # by the time we're calling this method, it's going to report the adapter (if we know it) but
95
+ # return 0 as the version. It won't report anything when using a custom adapter we don't
96
+ # identify.
97
+ #
98
+ # Returns a Hash<adapter_alias, version>
99
+ #
100
+ def meta_header_adapter
101
+ if @transport_class == Transport::HTTP::Faraday
102
+ version = '0'
103
+ adapter_version = case @arguments[:adapter]
104
+ when :patron
105
+ version = Patron::VERSION if defined?(::Patron::VERSION)
106
+ {pt: version}
107
+ when :net_http
108
+ version = if defined?(Net::HTTP::VERSION)
109
+ Net::HTTP::VERSION
110
+ elsif defined?(Net::HTTP::HTTPVersion)
111
+ Net::HTTP::HTTPVersion
112
+ end
113
+ {nh: version}
114
+ when :typhoeus
115
+ version = Typhoeus::VERSION if defined?(::Typhoeus::VERSION)
116
+ {ty: version}
117
+ when :httpclient
118
+ version = HTTPClient::VERSION if defined?(HTTPClient::VERSION)
119
+ {hc: version}
120
+ when :net_http_persistent
121
+ version = Net::HTTP::Persistent::VERSION if defined?(Net::HTTP::Persistent::VERSION)
122
+ {np: version}
123
+ else
124
+ {}
125
+ end
126
+ {fd: Faraday::VERSION}.merge(adapter_version)
127
+ elsif defined?(Transport::HTTP::Curb) && @transport_class == Transport::HTTP::Curb
128
+ {cl: Curl::CURB_VERSION}
129
+ elsif defined?(Transport::HTTP::Manticore) && @transport_class == Transport::HTTP::Manticore
130
+ {mc: Manticore::VERSION}
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -6,7 +6,7 @@
6
6
  # not use this file except in compliance with the License.
7
7
  # You may obtain a copy of the License at
8
8
  #
9
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
10
  #
11
11
  # Unless required by applicable law or agreed to in writing,
12
12
  # software distributed under the License is distributed on an
@@ -6,7 +6,7 @@
6
6
  # not use this file except in compliance with the License.
7
7
  # You may obtain a copy of the License at
8
8
  #
9
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
10
  #
11
11
  # Unless required by applicable law or agreed to in writing,
12
12
  # software distributed under the License is distributed on an
@@ -35,7 +35,7 @@ module Elasticsearch
35
35
  attr_reader :hosts, :options, :connections, :counter, :last_request_at, :protocol
36
36
  attr_accessor :serializer, :sniffer, :logger, :tracer,
37
37
  :reload_connections, :reload_after,
38
- :resurrect_after, :max_retries
38
+ :resurrect_after
39
39
 
40
40
  # Creates a new transport object
41
41
  #
@@ -47,7 +47,7 @@ module Elasticsearch
47
47
  #
48
48
  # @see Client#initialize
49
49
  #
50
- def initialize(arguments={}, &block)
50
+ def initialize(arguments = {}, &block)
51
51
  @state_mutex = Mutex.new
52
52
 
53
53
  @hosts = arguments[:hosts] || []
@@ -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) )
@@ -71,7 +72,6 @@ module Elasticsearch
71
72
  @reload_connections = options[:reload_connections]
72
73
  @reload_after = options[:reload_connections].is_a?(Integer) ? options[:reload_connections] : DEFAULT_RELOAD_AFTER
73
74
  @resurrect_after = options[:resurrect_after] || DEFAULT_RESURRECT_AFTER
74
- @max_retries = options[:retry_on_failure].is_a?(Integer) ? options[:retry_on_failure] : DEFAULT_MAX_RETRIES
75
75
  @retry_on_status = Array(options[:retry_on_status]).map { |d| d.to_i }
76
76
  end
77
77
 
@@ -202,7 +202,7 @@ module Elasticsearch
202
202
  ( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
203
203
  trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
204
204
  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?
205
+ trace_command += " -H '#{headers.collect { |k,v| "#{k}: #{v}" }.join(", ")}'" if headers && !headers.empty?
206
206
  trace_command += " '#{trace_url}'#{trace_body}\n"
207
207
  tracer.info trace_command
208
208
  tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
@@ -234,8 +234,9 @@ module Elasticsearch
234
234
  def __full_url(host)
235
235
  url = "#{host[:protocol]}://"
236
236
  url += "#{CGI.escape(host[:user])}:#{CGI.escape(host[:password])}@" if host[:user]
237
- url += "#{host[:host]}:#{host[:port]}"
238
- url += "#{host[:path]}" if host[:path]
237
+ url += host[:host]
238
+ url += ":#{host[:port]}" if host[:port]
239
+ url += host[:path] if host[:path]
239
240
  url
240
241
  end
241
242
 
@@ -257,10 +258,18 @@ module Elasticsearch
257
258
  # @raise [ServerError] If request failed on server
258
259
  # @raise [Error] If no connection is available
259
260
  #
260
- def perform_request(method, path, params={}, body=nil, headers=nil, &block)
261
- raise NoMethodError, "Implement this method in your transport class" unless block_given?
261
+ def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {}, &block)
262
+ raise NoMethodError, 'Implement this method in your transport class' unless block_given?
263
+
262
264
  start = Time.now
263
265
  tries = 0
266
+ reload_on_failure = opts.fetch(:reload_on_failure, @options[:reload_on_failure])
267
+
268
+ max_retries = if opts.key?(:retry_on_failure)
269
+ opts[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : opts[:retry_on_failure]
270
+ elsif options.key?(:retry_on_failure)
271
+ options[:retry_on_failure] === true ? DEFAULT_MAX_RETRIES : options[:retry_on_failure]
272
+ end
264
273
 
265
274
  params = params.clone
266
275
 
@@ -268,15 +277,15 @@ module Elasticsearch
268
277
 
269
278
  begin
270
279
  tries += 1
271
- connection = get_connection or raise Error.new("Cannot get new connection from pool.")
280
+ connection = get_connection or raise Error.new('Cannot get new connection from pool.')
272
281
 
273
282
  if connection.connection.respond_to?(:params) && connection.connection.params.respond_to?(:to_hash)
274
283
  params = connection.connection.params.merge(params.to_hash)
275
284
  end
276
285
 
277
- url = connection.full_url(path, params)
286
+ url = connection.full_url(path, params)
278
287
 
279
- response = block.call(connection, url)
288
+ response = block.call(connection, url)
280
289
 
281
290
  connection.healthy! if connection.failures > 0
282
291
 
@@ -286,7 +295,7 @@ module Elasticsearch
286
295
  rescue Elasticsearch::Transport::Transport::ServerError => e
287
296
  if response && @retry_on_status.include?(response.status)
288
297
  log_warn "[#{e.class}] Attempt #{tries} to get response from #{url}"
289
- if tries <= max_retries
298
+ if tries <= (max_retries || DEFAULT_MAX_RETRIES)
290
299
  retry
291
300
  else
292
301
  log_fatal "[#{e.class}] Cannot get response from #{url} after #{tries} tries"
@@ -301,12 +310,12 @@ module Elasticsearch
301
310
 
302
311
  connection.dead!
303
312
 
304
- if @options[:reload_on_failure] and tries < connections.all.size
313
+ if reload_on_failure and tries < connections.all.size
305
314
  log_warn "[#{e.class}] Reloading connections (attempt #{tries} of #{connections.all.size})"
306
315
  reload_connections! and retry
307
316
  end
308
317
 
309
- if @options[:retry_on_failure]
318
+ if max_retries
310
319
  log_warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}"
311
320
  if tries <= max_retries
312
321
  retry
@@ -328,7 +337,7 @@ module Elasticsearch
328
337
 
329
338
  if response.status.to_i >= 300
330
339
  __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
340
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
332
341
 
333
342
  # Log the failure only when `ignore` doesn't match the response status
334
343
  unless ignore.include?(response.status.to_i)
@@ -345,7 +354,9 @@ module Elasticsearch
345
354
  __log_response method, path, params, body, url, response, json, took, duration
346
355
  end
347
356
 
348
- __trace method, path, params, headers, body, url, response, json, took, duration if tracer
357
+ __trace method, path, params, connection.connection.headers, body, url, response, nil, 'N/A', duration if tracer
358
+
359
+ warnings(response.headers['warning']) if response.headers&.[]('warning')
349
360
 
350
361
  Response.new response.status, json || response.body, response.headers
351
362
  ensure
@@ -360,7 +371,71 @@ module Elasticsearch
360
371
  def host_unreachable_exceptions
361
372
  [Errno::ECONNREFUSED]
362
373
  end
374
+
375
+ private
376
+
377
+ USER_AGENT_STR = 'User-Agent'.freeze
378
+ USER_AGENT_REGEX = /user\-?\_?agent/
379
+ CONTENT_TYPE_STR = 'Content-Type'.freeze
380
+ CONTENT_TYPE_REGEX = /content\-?\_?type/
381
+ DEFAULT_CONTENT_TYPE = 'application/json'.freeze
382
+ GZIP = 'gzip'.freeze
383
+ ACCEPT_ENCODING = 'Accept-Encoding'.freeze
384
+ GZIP_FIRST_TWO_BYTES = '1f8b'.freeze
385
+ HEX_STRING_DIRECTIVE = 'H*'.freeze
386
+ RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
387
+
388
+ def decompress_response(body)
389
+ return body unless use_compression?
390
+ return body unless gzipped?(body)
391
+
392
+ io = StringIO.new(body)
393
+ gzip_reader = if RUBY_ENCODING
394
+ Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
395
+ else
396
+ Zlib::GzipReader.new(io)
397
+ end
398
+ gzip_reader.read
399
+ end
400
+
401
+ def gzipped?(body)
402
+ body[0..1].unpack(HEX_STRING_DIRECTIVE)[0] == GZIP_FIRST_TWO_BYTES
403
+ end
404
+
405
+ def use_compression?
406
+ @compression
407
+ end
408
+
409
+ def apply_headers(client, options)
410
+ headers = options[:headers] || {}
411
+ headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
412
+ headers[USER_AGENT_STR] = find_value(headers, USER_AGENT_REGEX) || user_agent_header(client)
413
+ client.headers[ACCEPT_ENCODING] = GZIP if use_compression?
414
+ client.headers.merge!(headers)
415
+ end
416
+
417
+ def find_value(hash, regex)
418
+ key_value = hash.find { |k,v| k.to_s.downcase =~ regex }
419
+ if key_value
420
+ hash.delete(key_value[0])
421
+ key_value[1]
422
+ end
423
+ end
424
+
425
+ def user_agent_header(client)
426
+ @user_agent ||= begin
427
+ meta = ["RUBY_VERSION: #{RUBY_VERSION}"]
428
+ if RbConfig::CONFIG && RbConfig::CONFIG['host_os']
429
+ meta << "#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase} #{RbConfig::CONFIG['target_cpu']}"
430
+ end
431
+ "elasticsearch-ruby/#{VERSION} (#{meta.join('; ')})"
432
+ end
433
+ end
434
+
435
+ def warnings(warning)
436
+ warn("warning: #{warning}")
437
+ end
363
438
  end
364
439
  end
365
440
  end
366
- end
441
+ end
@@ -6,7 +6,7 @@
6
6
  # not use this file except in compliance with the License.
7
7
  # You may obtain a copy of the License at
8
8
  #
9
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
10
  #
11
11
  # Unless required by applicable law or agreed to in writing,
12
12
  # software distributed under the License is distributed on an
@@ -78,16 +78,13 @@ module Elasticsearch
78
78
 
79
79
  # Returns a connection.
80
80
  #
81
- # If there are no alive connections, resurrects a connection with least failures.
81
+ # If there are no alive connections, returns a connection with least failures.
82
82
  # Delegates to selector's `#select` method to get the connection.
83
83
  #
84
84
  # @return [Connection]
85
85
  #
86
86
  def get_connection(options={})
87
- if connections.empty? && dead_connection = dead.sort { |a,b| a.failures <=> b.failures }.first
88
- dead_connection.alive!
89
- end
90
- selector.select(options)
87
+ selector.select(options) || @connections.min_by(&:failures)
91
88
  end
92
89
 
93
90
  def each(&block)