elasticsearch-transport 7.1.0 → 7.13.3

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.
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)