elasticsearch-transport-pixlee 1.0.13

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