elasticsearch-transport-pixlee 1.0.13

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE.txt +13 -0
  5. data/README.md +441 -0
  6. data/Rakefile +80 -0
  7. data/elasticsearch-transport.gemspec +74 -0
  8. data/lib/elasticsearch-transport.rb +1 -0
  9. data/lib/elasticsearch/transport.rb +30 -0
  10. data/lib/elasticsearch/transport/client.rb +195 -0
  11. data/lib/elasticsearch/transport/transport/base.rb +261 -0
  12. data/lib/elasticsearch/transport/transport/connections/collection.rb +93 -0
  13. data/lib/elasticsearch/transport/transport/connections/connection.rb +121 -0
  14. data/lib/elasticsearch/transport/transport/connections/selector.rb +63 -0
  15. data/lib/elasticsearch/transport/transport/errors.rb +73 -0
  16. data/lib/elasticsearch/transport/transport/http/curb.rb +87 -0
  17. data/lib/elasticsearch/transport/transport/http/faraday.rb +60 -0
  18. data/lib/elasticsearch/transport/transport/http/manticore.rb +124 -0
  19. data/lib/elasticsearch/transport/transport/response.rb +21 -0
  20. data/lib/elasticsearch/transport/transport/serializer/multi_json.rb +36 -0
  21. data/lib/elasticsearch/transport/transport/sniffer.rb +46 -0
  22. data/lib/elasticsearch/transport/version.rb +5 -0
  23. data/test/integration/client_test.rb +144 -0
  24. data/test/integration/transport_test.rb +73 -0
  25. data/test/profile/client_benchmark_test.rb +125 -0
  26. data/test/test_helper.rb +76 -0
  27. data/test/unit/client_test.rb +274 -0
  28. data/test/unit/connection_collection_test.rb +88 -0
  29. data/test/unit/connection_selector_test.rb +64 -0
  30. data/test/unit/connection_test.rb +100 -0
  31. data/test/unit/response_test.rb +15 -0
  32. data/test/unit/serializer_test.rb +16 -0
  33. data/test/unit/sniffer_test.rb +145 -0
  34. data/test/unit/transport_base_test.rb +478 -0
  35. data/test/unit/transport_curb_test.rb +97 -0
  36. data/test/unit/transport_faraday_test.rb +140 -0
  37. data/test/unit/transport_manticore_test.rb +118 -0
  38. metadata +408 -0
data/Rakefile ADDED
@@ -0,0 +1,80 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc "Run unit tests"
4
+ task :default => 'test:unit'
5
+ task :test => 'test:unit'
6
+
7
+ # ----- Test tasks ------------------------------------------------------------
8
+
9
+ require 'rake/testtask'
10
+ namespace :test do
11
+ task :ci_reporter do
12
+ ENV['CI_REPORTS'] ||= 'tmp/reports'
13
+ if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
14
+ require 'ci/reporter/rake/test_unit'
15
+ Rake::Task['ci:setup:testunit'].invoke
16
+ else
17
+ require 'ci/reporter/rake/minitest'
18
+ Rake::Task['ci:setup:minitest'].invoke
19
+ end
20
+ end
21
+
22
+ Rake::TestTask.new(:unit) do |test|
23
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
24
+ test.libs << 'lib' << 'test'
25
+ test.test_files = FileList["test/unit/**/*_test.rb"]
26
+ # test.verbose = true
27
+ # test.warning = true
28
+ end
29
+
30
+ Rake::TestTask.new(:integration) do |test|
31
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
32
+ test.libs << 'lib' << 'test'
33
+ test.test_files = FileList["test/integration/**/*_test.rb"]
34
+ end
35
+
36
+ Rake::TestTask.new(:all) do |test|
37
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
38
+ test.libs << 'lib' << 'test'
39
+ test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"]
40
+ end
41
+
42
+ Rake::TestTask.new(:profile) do |test|
43
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
44
+ test.libs << 'lib' << 'test'
45
+ test.test_files = FileList["test/profile/**/*_test.rb"]
46
+ end
47
+
48
+ namespace :cluster do
49
+ desc "Start Elasticsearch nodes for tests"
50
+ task :start do
51
+ $LOAD_PATH << File.expand_path('../lib', __FILE__) << File.expand_path('../test', __FILE__)
52
+ require 'elasticsearch/extensions/test/cluster'
53
+ Elasticsearch::Extensions::Test::Cluster.start
54
+ end
55
+
56
+ desc "Stop Elasticsearch nodes for tests"
57
+ task :stop do
58
+ $LOAD_PATH << File.expand_path('../lib', __FILE__) << File.expand_path('../test', __FILE__)
59
+ require 'elasticsearch/extensions/test/cluster'
60
+ Elasticsearch::Extensions::Test::Cluster.stop
61
+ end
62
+ end
63
+ end
64
+
65
+ # ----- Documentation tasks ---------------------------------------------------
66
+
67
+ require 'yard'
68
+ YARD::Rake::YardocTask.new(:doc) do |t|
69
+ t.options = %w| --embed-mixins --markup=markdown |
70
+ end
71
+
72
+ # ----- Code analysis tasks ---------------------------------------------------
73
+
74
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
75
+ require 'cane/rake_task'
76
+ Cane::RakeTask.new(:quality) do |cane|
77
+ cane.abc_max = 15
78
+ cane.no_style = true
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'elasticsearch/transport/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "elasticsearch-transport-pixlee"
8
+ s.version = Elasticsearch::Transport::VERSION
9
+ s.authors = ["Karel Minarik"]
10
+ s.email = ["karel.minarik@elasticsearch.org"]
11
+ s.summary = "Ruby client for Elasticsearch."
12
+ s.homepage = "https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport"
13
+ s.license = "Apache 2"
14
+
15
+ s.files = `git ls-files`.split($/)
16
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+ s.require_paths = ["lib"]
19
+
20
+ s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ]
21
+ s.rdoc_options = [ "--charset=UTF-8" ]
22
+
23
+ s.add_dependency "multi_json"
24
+ s.add_dependency "faraday"
25
+
26
+ if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
27
+ s.add_dependency "system_timer"
28
+ end
29
+
30
+ s.add_development_dependency "bundler", "> 1"
31
+ s.add_development_dependency "rake"
32
+
33
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
34
+ s.add_development_dependency "elasticsearch-extensions"
35
+ end
36
+
37
+ s.add_development_dependency "ansi"
38
+ s.add_development_dependency "shoulda-context"
39
+ s.add_development_dependency "mocha"
40
+ s.add_development_dependency "turn"
41
+ s.add_development_dependency "yard"
42
+ s.add_development_dependency "pry"
43
+ s.add_development_dependency "ci_reporter", "~> 1.9"
44
+
45
+ # Gems for testing integrations
46
+ s.add_development_dependency "curb" unless defined? JRUBY_VERSION
47
+ s.add_development_dependency "patron" unless defined? JRUBY_VERSION
48
+ s.add_development_dependency "typhoeus", '~> 0.6'
49
+ s.add_development_dependency "manticore", '~> 0.3.5' if defined? JRUBY_VERSION
50
+ s.add_development_dependency "hashie"
51
+
52
+ # Prevent unit test failures on Ruby 1.8
53
+ if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
54
+ s.add_development_dependency "test-unit", '~> 2'
55
+ s.add_development_dependency "json"
56
+ end
57
+
58
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
59
+ s.add_development_dependency "minitest", "~> 4.0"
60
+ s.add_development_dependency "ruby-prof" unless defined?(JRUBY_VERSION) || defined?(Rubinius)
61
+ s.add_development_dependency "require-prof" unless defined?(JRUBY_VERSION) || defined?(Rubinius)
62
+ s.add_development_dependency "simplecov"
63
+ s.add_development_dependency "simplecov-rcov"
64
+ s.add_development_dependency "cane"
65
+ end
66
+
67
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '2.2'
68
+ s.add_development_dependency "test-unit", '~> 2'
69
+ end
70
+
71
+ s.description = <<-DESC.gsub(/^ /, '')
72
+ Ruby client for Elasticsearch. See the `elasticsearch` gem for full integration.
73
+ DESC
74
+ end
@@ -0,0 +1 @@
1
+ require 'elasticsearch/transport'
@@ -0,0 +1,30 @@
1
+ require "uri"
2
+ require "time"
3
+ require "timeout"
4
+ require "multi_json"
5
+ require "faraday"
6
+
7
+ require "elasticsearch/transport/transport/serializer/multi_json"
8
+ require "elasticsearch/transport/transport/sniffer"
9
+ require "elasticsearch/transport/transport/response"
10
+ require "elasticsearch/transport/transport/errors"
11
+ require "elasticsearch/transport/transport/base"
12
+ require "elasticsearch/transport/transport/connections/selector"
13
+ require "elasticsearch/transport/transport/connections/connection"
14
+ require "elasticsearch/transport/transport/connections/collection"
15
+ require "elasticsearch/transport/transport/http/faraday"
16
+ require "elasticsearch/transport/client"
17
+
18
+ require "elasticsearch/transport/version"
19
+
20
+ module Elasticsearch
21
+ module Client
22
+
23
+ # A convenience wrapper for {::Elasticsearch::Transport::Client#initialize}.
24
+ #
25
+ def new(arguments={})
26
+ Elasticsearch::Transport::Client.new(arguments)
27
+ end
28
+ extend self
29
+ end
30
+ end
@@ -0,0 +1,195 @@
1
+ module Elasticsearch
2
+ module Transport
3
+
4
+ # Handles communication with an Elasticsearch cluster.
5
+ #
6
+ # See {file:README.md README} for usage and code examples.
7
+ #
8
+ class Client
9
+ DEFAULT_TRANSPORT_CLASS = Transport::HTTP::Faraday
10
+
11
+ DEFAULT_LOGGER = lambda do
12
+ require 'logger'
13
+ logger = Logger.new(STDERR)
14
+ logger.progname = 'elasticsearch'
15
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime}: #{msg}\n" }
16
+ logger
17
+ end
18
+
19
+ DEFAULT_TRACER = lambda do
20
+ require 'logger'
21
+ logger = Logger.new(STDERR)
22
+ logger.progname = 'elasticsearch.tracer'
23
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
24
+ logger
25
+ end
26
+
27
+ # Returns the transport object.
28
+ #
29
+ # @see Elasticsearch::Transport::Transport::Base
30
+ # @see Elasticsearch::Transport::Transport::HTTP::Faraday
31
+ #
32
+ attr_accessor :transport
33
+
34
+ # Create a client connected to an Elasticsearch cluster.
35
+ #
36
+ # Specify the URL via arguments or set the `ELASTICSEARCH_URL` environment variable.
37
+ #
38
+ # @option arguments [String,Array] :hosts Single host passed as a String or Hash, or multiple hosts
39
+ # passed as an Array; `host` or `url` keys are also valid
40
+ #
41
+ # @option arguments [Boolean] :log Use the default logger (disabled by default)
42
+ #
43
+ # @option arguments [Boolean] :trace Use the default tracer (disabled by default)
44
+ #
45
+ # @option arguments [Object] :logger An instance of a Logger-compatible object
46
+ #
47
+ # @option arguments [Object] :tracer An instance of a Logger-compatible object
48
+ #
49
+ # @option arguments [Number] :resurrect_after After how many seconds a dead connection should be tried again
50
+ #
51
+ # @option arguments [Boolean,Number] :reload_connections Reload connections after X requests (false by default)
52
+ #
53
+ # @option arguments [Boolean] :randomize_hosts Shuffle connections on initialization and reload (false by default)
54
+ #
55
+ # @option arguments [Integer] :sniffer_timeout Timeout for reloading connections in seconds (1 by default)
56
+ #
57
+ # @option arguments [Boolean,Number] :retry_on_failure Retry X times when request fails before raising and
58
+ # exception (false by default)
59
+ #
60
+ # @option arguments [Boolean] :reload_on_failure Reload connections after failure (false by default)
61
+ #
62
+ # @option arguments [Integer] :request_timeout The request timeout to be passed to transport in options
63
+ #
64
+ # @option arguments [Symbol] :adapter A specific adapter for Faraday (e.g. `:patron`)
65
+ #
66
+ # @option arguments [Hash] :transport_options Options to be passed to the `Faraday::Connection` constructor
67
+ #
68
+ # @option arguments [Constant] :transport_class A specific transport class to use, will be initialized by
69
+ # the client and passed hosts and all arguments
70
+ #
71
+ # @option arguments [Object] :transport A specific transport instance
72
+ #
73
+ # @option arguments [Constant] :serializer_class A specific serializer class to use, will be initialized by
74
+ # the transport and passed the transport instance
75
+ #
76
+ # @option arguments [Constant] :selector An instance of selector strategy implemented with
77
+ # {Elasticsearch::Transport::Transport::Connections::Selector::Base}.
78
+ #
79
+ # @option arguments [String] :send_get_body_as Specify the HTTP method to use for GET requests with a body.
80
+ # (Default: GET)
81
+ #
82
+ def initialize(arguments={})
83
+ hosts = arguments[:hosts] || \
84
+ arguments[:host] || \
85
+ arguments[:url] || \
86
+ arguments[:urls] || \
87
+ ENV.fetch('ELASTICSEARCH_URL', 'localhost:9200')
88
+
89
+ arguments[:logger] ||= arguments[:log] ? DEFAULT_LOGGER.call() : nil
90
+ arguments[:tracer] ||= arguments[:trace] ? DEFAULT_TRACER.call() : nil
91
+ arguments[:reload_connections] ||= false
92
+ arguments[:retry_on_failure] ||= false
93
+ arguments[:reload_on_failure] ||= false
94
+ arguments[:randomize_hosts] ||= false
95
+ arguments[:transport_options] ||= {}
96
+
97
+ arguments[:transport_options].update(:request => { :timeout => arguments[:request_timeout] } ) if arguments[:request_timeout]
98
+
99
+ @send_get_body_as = arguments[:send_get_body_as] || 'GET'
100
+
101
+ transport_class = arguments[:transport_class] || DEFAULT_TRANSPORT_CLASS
102
+
103
+ @transport = arguments[:transport] || begin
104
+ if transport_class == Transport::HTTP::Faraday
105
+ transport_class.new(:hosts => __extract_hosts(hosts, arguments), :options => arguments) do |faraday|
106
+ faraday.adapter(arguments[:adapter] || __auto_detect_adapter)
107
+ end
108
+ else
109
+ transport_class.new(:hosts => __extract_hosts(hosts, arguments), :options => arguments)
110
+ end
111
+ end
112
+ end
113
+
114
+ # Performs a request through delegation to {#transport}.
115
+ #
116
+ def perform_request(method, path, params={}, body=nil)
117
+ method = @send_get_body_as if 'GET' == method && body
118
+
119
+ transport.perform_request method, path, params, body
120
+ end
121
+
122
+ # Normalizes and returns hosts configuration.
123
+ #
124
+ # Arrayifies the `hosts_config` argument and extracts `host` and `port` info from strings.
125
+ # Performs shuffling when the `randomize_hosts` option is set.
126
+ #
127
+ # TODO: Refactor, so it's available in Elasticsearch::Transport::Base as well
128
+ #
129
+ # @return [Array<Hash>]
130
+ # @raise [ArgumentError]
131
+ #
132
+ # @api private
133
+ #
134
+ def __extract_hosts(hosts_config, options={})
135
+ if hosts_config.is_a?(Hash)
136
+ hosts = [ hosts_config ]
137
+ else
138
+ if hosts_config.is_a?(String) && hosts_config.include?(',')
139
+ hosts = hosts_config.split(/\s*,\s*/)
140
+ else
141
+ hosts = Array(hosts_config)
142
+ end
143
+ end
144
+
145
+ result = hosts.map do |host|
146
+ host_parts = case host
147
+ when String
148
+ if host =~ /^[a-z]+\:\/\//
149
+ uri = URI.parse(host)
150
+ { :scheme => uri.scheme, :user => uri.user, :password => uri.password, :host => uri.host, :path => uri.path, :port => uri.port }
151
+ else
152
+ host, port = host.split(':')
153
+ { :host => host, :port => port }
154
+ end
155
+ when URI
156
+ { :scheme => host.scheme, :user => host.user, :password => host.password, :host => host.host, :path => host.path, :port => host.port }
157
+ when Hash
158
+ host
159
+ else
160
+ raise ArgumentError, "Please pass host as a String, URI or Hash -- #{host.class} given."
161
+ end
162
+
163
+ host_parts[:port] = host_parts[:port].to_i unless host_parts[:port].nil?
164
+ host_parts
165
+ end
166
+
167
+ result.shuffle! if options[:randomize_hosts]
168
+ result
169
+ end
170
+
171
+ # Auto-detect the best adapter (HTTP "driver") available, based on libraries
172
+ # loaded by the user, preferring those with persistent connections
173
+ # ("keep-alive") by default
174
+ #
175
+ # @return [Symbol]
176
+ #
177
+ # @api private
178
+ #
179
+ def __auto_detect_adapter
180
+ case
181
+ when defined?(::Patron)
182
+ :patron
183
+ when defined?(::Typhoeus)
184
+ :typhoeus
185
+ when defined?(::HTTPClient)
186
+ :httpclient
187
+ when defined?(::Net::HTTP::Persistent)
188
+ :net_http_persistent
189
+ else
190
+ ::Faraday.default_adapter
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,261 @@
1
+ module Elasticsearch
2
+ module Transport
3
+ module Transport
4
+
5
+ # @abstract Module with common functionality for transport implementations.
6
+ #
7
+ module Base
8
+ DEFAULT_PORT = 9200
9
+ DEFAULT_PROTOCOL = 'http'
10
+ DEFAULT_RELOAD_AFTER = 10_000 # Requests
11
+ DEFAULT_RESURRECT_AFTER = 60 # Seconds
12
+ DEFAULT_MAX_RETRIES = 3 # Requests
13
+ DEFAULT_SERIALIZER_CLASS = Serializer::MultiJson
14
+
15
+ attr_reader :hosts, :options, :connections, :counter, :last_request_at, :protocol
16
+ attr_accessor :serializer, :sniffer, :logger, :tracer,
17
+ :reload_connections, :reload_after,
18
+ :resurrect_after, :max_retries
19
+
20
+ # Creates a new transport object.
21
+ #
22
+ # @param arguments [Hash] Settings and options for the transport
23
+ # @param block [Proc] Lambda or Proc which can be evaluated in the context of the "session" object
24
+ #
25
+ # @option arguments [Array] :hosts An Array of normalized hosts information
26
+ # @option arguments [Array] :options A Hash with options (usually passed by {Client})
27
+ #
28
+ # @see Client#initialize
29
+ #
30
+ def initialize(arguments={}, &block)
31
+ @hosts = arguments[:hosts] || []
32
+ @options = arguments[:options] || {}
33
+ @block = block
34
+ @connections = __build_connections
35
+
36
+ @serializer = options[:serializer] || ( options[:serializer_class] ? options[:serializer_class].new(self) : DEFAULT_SERIALIZER_CLASS.new(self) )
37
+ @protocol = options[:protocol] || DEFAULT_PROTOCOL
38
+
39
+ @logger = options[:logger]
40
+ @tracer = options[:tracer]
41
+
42
+ @sniffer = options[:sniffer_class] ? options[:sniffer_class].new(self) : Sniffer.new(self)
43
+ @counter = 0
44
+ @last_request_at = Time.now
45
+ @reload_connections = options[:reload_connections]
46
+ @reload_after = options[:reload_connections].is_a?(Fixnum) ? options[:reload_connections] : DEFAULT_RELOAD_AFTER
47
+ @resurrect_after = options[:resurrect_after] || DEFAULT_RESURRECT_AFTER
48
+ @max_retries = options[:retry_on_failure].is_a?(Fixnum) ? options[:retry_on_failure] : DEFAULT_MAX_RETRIES
49
+ end
50
+
51
+ # Returns a connection from the connection pool by delegating to {Connections::Collection#get_connection}.
52
+ #
53
+ # Resurrects dead connection if the `resurrect_after` timeout has passed.
54
+ # Increments the counter and performs connection reloading if the `reload_connections` option is set.
55
+ #
56
+ # @return [Connections::Connection]
57
+ # @see Connections::Collection#get_connection
58
+ #
59
+ def get_connection(options={})
60
+ resurrect_dead_connections! if Time.now > @last_request_at + @resurrect_after
61
+
62
+ connection = connections.get_connection(options)
63
+ @counter += 1
64
+
65
+ reload_connections! if reload_connections && counter % reload_after == 0
66
+ connection
67
+ end
68
+
69
+ # Reloads and replaces the connection collection based on cluster information.
70
+ #
71
+ # @see Sniffer#hosts
72
+ #
73
+ def reload_connections!
74
+ hosts = sniffer.hosts
75
+ __rebuild_connections :hosts => hosts, :options => options
76
+ self
77
+ rescue SnifferTimeoutError
78
+ logger.error "[SnifferTimeoutError] Timeout when reloading connections." if logger
79
+ self
80
+ end
81
+
82
+ # Tries to "resurrect" all eligible dead connections.
83
+ #
84
+ # @see Connections::Connection#resurrect!
85
+ #
86
+ def resurrect_dead_connections!
87
+ connections.dead.each { |c| c.resurrect! }
88
+ end
89
+
90
+ # Replaces the connections collection.
91
+ #
92
+ # @api private
93
+ #
94
+ def __rebuild_connections(arguments={})
95
+ @hosts = arguments[:hosts] || []
96
+ @options = arguments[:options] || {}
97
+ @connections = __build_connections
98
+ end
99
+
100
+ # Log request and response information.
101
+ #
102
+ # @api private
103
+ #
104
+ def __log(method, path, params, body, url, response, json, took, duration)
105
+ logger.info "#{method.to_s.upcase} #{url} " +
106
+ "[status:#{response.status}, request:#{sprintf('%.3fs', duration)}, query:#{took}]"
107
+ logger.debug "> #{__convert_to_json(body)}" if body
108
+ logger.debug "< #{response.body}"
109
+ end
110
+
111
+ # Log failed request.
112
+ #
113
+ # @api private
114
+ def __log_failed(response)
115
+ logger.fatal "[#{response.status}] #{response.body}"
116
+ end
117
+
118
+ # Trace the request in the `curl` format.
119
+ #
120
+ # @api private
121
+ def __trace(method, path, params, body, url, response, json, took, duration)
122
+ trace_url = "http://localhost:9200/#{path}?pretty" +
123
+ ( params.empty? ? '' : "&#{::Faraday::Utils::ParamsHash[params].to_query}" )
124
+ trace_body = body ? " -d '#{__convert_to_json(body, :pretty => true)}'" : ''
125
+ tracer.info "curl -X #{method.to_s.upcase} '#{trace_url}'#{trace_body}\n"
126
+ tracer.debug "# #{Time.now.iso8601} [#{response.status}] (#{format('%.3f', duration)}s)\n#"
127
+ tracer.debug json ? serializer.dump(json, :pretty => true).gsub(/^/, '# ').sub(/\}$/, "\n# }")+"\n" : "# #{response.body}\n"
128
+ end
129
+
130
+ # Raise error specific for the HTTP response status or a generic server error
131
+ #
132
+ # @api private
133
+ def __raise_transport_error(response)
134
+ error = ERRORS[response.status] || ServerError
135
+ raise error.new "[#{response.status}] #{response.body}"
136
+ end
137
+
138
+ # Converts any non-String object to JSON
139
+ #
140
+ # @api private
141
+ def __convert_to_json(o=nil, options={})
142
+ o = o.is_a?(String) ? o : serializer.dump(o, options)
143
+ end
144
+
145
+ # Returns a full URL based on information from host
146
+ #
147
+ # @param host [Hash] Host configuration passed in from {Client}
148
+ #
149
+ # @api private
150
+ def __full_url(host)
151
+ url = "#{host[:protocol]}://"
152
+ url += "#{host[:user]}:#{host[:password]}@" if host[:user]
153
+ url += "#{host[:host]}:#{host[:port]}"
154
+ url += "#{host[:path]}" if host[:path]
155
+ url
156
+ end
157
+
158
+ # Performs a request to Elasticsearch, while handling logging, tracing, marking dead connections,
159
+ # retrying the request and reloading the connections.
160
+ #
161
+ # @abstract The transport implementation has to implement this method either in full,
162
+ # or by invoking this method with a block. See {HTTP::Faraday#perform_request} for an example.
163
+ #
164
+ # @param method [String] Request method
165
+ # @param path [String] The API endpoint
166
+ # @param params [Hash] Request parameters (will be serialized by {Connections::Connection#full_url})
167
+ # @param body [Hash] Request body (will be serialized by the {#serializer})
168
+ # @param block [Proc] Code block to evaluate, passed from the implementation
169
+ #
170
+ # @return [Response]
171
+ # @raise [NoMethodError] If no block is passed
172
+ # @raise [ServerError] If request failed on server
173
+ # @raise [Error] If no connection is available
174
+ #
175
+ def perform_request(method, path, params={}, body=nil, &block)
176
+ raise NoMethodError, "Implement this method in your transport class" unless block_given?
177
+ start = Time.now if logger || tracer
178
+ tries = 0
179
+
180
+ begin
181
+ tries += 1
182
+ connection = get_connection or raise Error.new("Cannot get new connection from pool.")
183
+
184
+ if connection.connection.respond_to?(:params) && connection.connection.params.respond_to?(:to_hash)
185
+ params = connection.connection.params.merge(params.to_hash)
186
+ end
187
+
188
+ url = connection.full_url(path, params)
189
+
190
+ response = block.call(connection, url)
191
+
192
+ connection.healthy! if connection.failures > 0
193
+
194
+ rescue *host_unreachable_exceptions => e
195
+ logger.error "[#{e.class}] #{e.message} #{connection.host.inspect}" if logger
196
+
197
+ connection.dead!
198
+
199
+ if @options[:reload_on_failure] and tries < connections.all.size
200
+ logger.warn "[#{e.class}] Reloading connections (attempt #{tries} of #{connections.all.size})" if logger
201
+ reload_connections! and retry
202
+ end
203
+
204
+ if @options[:retry_on_failure]
205
+ logger.warn "[#{e.class}] Attempt #{tries} connecting to #{connection.host.inspect}" if logger
206
+ if tries <= max_retries
207
+ retry
208
+ else
209
+ logger.fatal "[#{e.class}] Cannot connect to #{connection.host.inspect} after #{tries} tries" if logger
210
+ raise e
211
+ end
212
+ else
213
+ raise e
214
+ end
215
+
216
+ rescue Exception => e
217
+ logger.fatal "[#{e.class}] #{e.message} (#{connection.host.inspect if connection})" if logger
218
+ raise e
219
+ end
220
+
221
+ duration = Time.now-start if logger || tracer
222
+
223
+ if response.status.to_i >= 300
224
+ __log method, path, params, body, url, response, nil, 'N/A', duration if logger
225
+ __trace method, path, params, body, url, response, nil, 'N/A', duration if tracer
226
+ __log_failed response if logger
227
+ __raise_transport_error response
228
+ end
229
+
230
+ json = serializer.load(response.body) if response.headers && response.headers["content-type"] =~ /json/
231
+ took = (json['took'] ? sprintf('%.3fs', json['took']/1000.0) : 'n/a') rescue 'n/a' if logger || tracer
232
+
233
+ __log method, path, params, body, url, response, json, took, duration if logger
234
+ __trace method, path, params, body, url, response, json, took, duration if tracer
235
+
236
+ Response.new response.status, json || response.body, response.headers
237
+ ensure
238
+ @last_request_at = Time.now
239
+ end
240
+
241
+ # @abstract Returns an Array of connection errors specific to the transport implementation.
242
+ # See {HTTP::Faraday#host_unreachable_exceptions} for an example.
243
+ #
244
+ # @return [Array]
245
+ #
246
+ def host_unreachable_exceptions
247
+ [Errno::ECONNREFUSED, Elasticsearch::Transport::Transport::Errors::BadGateway]
248
+ end
249
+
250
+ # @abstract A transport implementation must implement this method.
251
+ # See {HTTP::Faraday#__build_connections} for an example.
252
+ #
253
+ # @return [Connections::Collection]
254
+ # @api private
255
+ def __build_connections
256
+ raise NoMethodError, "Implement this method in your class"
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end