yoga_pants 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +53 -0
- data/Rakefile +11 -0
- data/fixtures/vcr_cassettes/before_block.yml +165 -0
- data/fixtures/vcr_cassettes/bulk.yml +69 -0
- data/fixtures/vcr_cassettes/bulk_document_index_error.yml +69 -0
- data/fixtures/vcr_cassettes/bulk_error.yml +355 -0
- data/fixtures/vcr_cassettes/error.yml +290 -0
- data/fixtures/vcr_cassettes/failover_refused.yml +25 -0
- data/fixtures/vcr_cassettes/failover_timeout.yml +25 -0
- data/fixtures/vcr_cassettes/indexing.yml +48 -0
- data/fixtures/vcr_cassettes/invalid_request.yml +267 -0
- data/fixtures/vcr_cassettes/missing.yml +267 -0
- data/fixtures/vcr_cassettes/multi_search.yml +37 -0
- data/lib/yoga_pants/client.rb +144 -0
- data/lib/yoga_pants/connection.rb +150 -0
- data/lib/yoga_pants/json.rb +25 -0
- data/lib/yoga_pants/version.rb +3 -0
- data/lib/yoga_pants.rb +10 -0
- data/spec/integration/basic_spec.rb +205 -0
- data/spec/spec_helper.rb +9 -0
- data/yoga_pants.gemspec +26 -0
- metadata +138 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
module YogaPants
|
|
2
|
+
class Client
|
|
3
|
+
# This class will handle:
|
|
4
|
+
# * connecting to ES nodes
|
|
5
|
+
# * failing over to nodes in a list
|
|
6
|
+
# * ES-specific error handling
|
|
7
|
+
|
|
8
|
+
class RequestError < RuntimeError
|
|
9
|
+
attr_reader :http_error
|
|
10
|
+
|
|
11
|
+
def initialize(message, http_error = nil)
|
|
12
|
+
@http_error = nil
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_accessor :hosts, :options, :active_host
|
|
18
|
+
|
|
19
|
+
def initialize(hosts, options = {})
|
|
20
|
+
@hosts = [hosts].flatten.freeze # Accept 1 or more hosts
|
|
21
|
+
@options = options
|
|
22
|
+
@max_retries = options[:max_retries] || 10
|
|
23
|
+
@retries = 0
|
|
24
|
+
reset_hosts
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_hosts
|
|
28
|
+
@active_hosts = hosts.dup
|
|
29
|
+
@active_host = @active_hosts.shift
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(path, args = {})
|
|
33
|
+
with_error_handling do
|
|
34
|
+
connection.get(path, args)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def post(path, args = {})
|
|
39
|
+
with_error_handling do
|
|
40
|
+
connection.post(path, args)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def put(path, args = {})
|
|
45
|
+
with_error_handling do
|
|
46
|
+
connection.put(path, args)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete(path, args = {})
|
|
51
|
+
with_error_handling do
|
|
52
|
+
connection.delete(path, args)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
BULK_OPERATIONS_WITH_DATA = [:index, :create].freeze
|
|
57
|
+
def bulk(path, operations, args = {})
|
|
58
|
+
path = path.sub(%r{/(?:_bulk)?$}, '/_bulk')
|
|
59
|
+
|
|
60
|
+
with_error_handling do
|
|
61
|
+
payload = StringIO.new
|
|
62
|
+
|
|
63
|
+
operations.each do |action, metadata, data|
|
|
64
|
+
payload << JSON.dump({action => metadata})
|
|
65
|
+
payload << "\n"
|
|
66
|
+
if BULK_OPERATIONS_WITH_DATA.include?(action.to_sym)
|
|
67
|
+
payload << JSON.dump(data)
|
|
68
|
+
payload << "\n"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
payload.rewind
|
|
73
|
+
connection.post(path, :query_string => args, :body => payload.read)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def multi_search(path, operations, args={})
|
|
78
|
+
path = path.sub(%r{/?(?:_msearch)?$}, '/_msearch')
|
|
79
|
+
|
|
80
|
+
with_error_handling do
|
|
81
|
+
payload = StringIO.new
|
|
82
|
+
|
|
83
|
+
operations.each do |header, body|
|
|
84
|
+
payload << JSON.dump(header) << "\n"
|
|
85
|
+
payload << JSON.dump(body) << "\n"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
payload.rewind
|
|
89
|
+
connection.get(path, :query_string => args, :body => payload.read)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def exists?(path, args = {})
|
|
94
|
+
with_error_handling do
|
|
95
|
+
begin
|
|
96
|
+
if path.count("/") >= 3 # More than
|
|
97
|
+
connection.get(path, args)
|
|
98
|
+
else
|
|
99
|
+
connection.head(path).status_code == 200
|
|
100
|
+
end
|
|
101
|
+
rescue Connection::HTTPError => e
|
|
102
|
+
if e.status_code == 404
|
|
103
|
+
false
|
|
104
|
+
else
|
|
105
|
+
raise e
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def reset
|
|
112
|
+
connection.reset
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def connection
|
|
118
|
+
@connection ||= Connection.new(active_host, options[:connection])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def pick_next_host
|
|
122
|
+
@active_host = @active_hosts.shift
|
|
123
|
+
@connection = nil
|
|
124
|
+
reset_hosts if active_host.nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def with_error_handling(&block)
|
|
128
|
+
block.call.tap do
|
|
129
|
+
@retries = 0
|
|
130
|
+
end
|
|
131
|
+
rescue Connection::HTTPError => e
|
|
132
|
+
|
|
133
|
+
if @retries <= @max_retries
|
|
134
|
+
@retries += 1
|
|
135
|
+
pick_next_host
|
|
136
|
+
retry
|
|
137
|
+
elsif e.body.is_a?(Hash) && error = e.body['error']
|
|
138
|
+
raise RequestError.new("ElasticSearch Error: #{error}", e).tap { |ex| ex.set_backtrace(e.backtrace) }
|
|
139
|
+
else
|
|
140
|
+
raise RequestError.new(e.message, e).tap { |ex| ex.set_backtrace(e.backtrace) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module YogaPants
|
|
2
|
+
class Connection
|
|
3
|
+
# This class is/will be an abstraction layer for the underlying
|
|
4
|
+
# HTTP client.
|
|
5
|
+
# TODO: Use https://github.com/rubiii/httpi so we don't have to deal
|
|
6
|
+
# with interfacing with multiple HTTP libraries
|
|
7
|
+
|
|
8
|
+
attr_accessor :host, :options
|
|
9
|
+
|
|
10
|
+
class HTTPError < RuntimeError
|
|
11
|
+
attr_reader :response, :status_code
|
|
12
|
+
def initialize(message, response = nil)
|
|
13
|
+
@response = response
|
|
14
|
+
if response
|
|
15
|
+
@status_code = response.status_code
|
|
16
|
+
super(message + "\nBody: #{response.body}")
|
|
17
|
+
else
|
|
18
|
+
super(message)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def body
|
|
23
|
+
return nil if response.nil?
|
|
24
|
+
@body ||= begin
|
|
25
|
+
JSON.load(response.body)
|
|
26
|
+
rescue MultiJson::DecodeError
|
|
27
|
+
response.body
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(host, options = {})
|
|
33
|
+
@host = host.chomp('/')
|
|
34
|
+
@options = options || {}
|
|
35
|
+
@http = HTTPClient.new
|
|
36
|
+
|
|
37
|
+
default_timeout = @options[:timeout] || 5
|
|
38
|
+
http.connect_timeout = @options[:connect_timeout] || default_timeout
|
|
39
|
+
http.send_timeout = @options[:send_timeout] || default_timeout
|
|
40
|
+
http.receive_timeout = @options[:receive_timeout] || default_timeout
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Body can be a string or hash
|
|
44
|
+
|
|
45
|
+
def get(path, args = {})
|
|
46
|
+
with_error_handling do
|
|
47
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
|
48
|
+
http.get(url_for(path), :query => query_string, :body => body)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def post(path, args = {})
|
|
54
|
+
with_error_handling do
|
|
55
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
|
56
|
+
response = http.post(url_for(path), :query => query_string, :body => body)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def put(path, args = {})
|
|
62
|
+
with_error_handling do
|
|
63
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
|
64
|
+
response = http.put(url_for(path), :query => query_string, :body => body)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete(path, args = {})
|
|
70
|
+
with_error_handling do
|
|
71
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
|
72
|
+
response = http.delete(url_for(path), :query => query_string, :body => body)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def head(path, args = {})
|
|
78
|
+
with_error_handling do
|
|
79
|
+
query_string, _ = parse_arguments(args)
|
|
80
|
+
http.head(url_for(path), :query => query_string)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reset
|
|
85
|
+
http.reset_all
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def parse_arguments_and_handle_response(args, &block)
|
|
91
|
+
query_string, body = parse_arguments(args)
|
|
92
|
+
parse_and_handle_response(
|
|
93
|
+
block.call(query_string, body)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_and_handle_response(response)
|
|
98
|
+
case response.status_code
|
|
99
|
+
when 200..299
|
|
100
|
+
JSON.load(response.body)
|
|
101
|
+
else
|
|
102
|
+
raise HTTPError.new("Error performing HTTP request: #{response.status_code} #{response.reason}", response)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_arguments(args)
|
|
107
|
+
[args[:query_string], jsonify_body(args[:body])]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_error_handling(&block)
|
|
111
|
+
block.call
|
|
112
|
+
rescue HTTPError => e
|
|
113
|
+
raise e
|
|
114
|
+
rescue Errno::ECONNREFUSED
|
|
115
|
+
raise HTTPError.new("Connection refused to #{host}")
|
|
116
|
+
rescue HTTPClient::ConnectTimeoutError
|
|
117
|
+
raise HTTPError.new("Connection timed out to #{host}")
|
|
118
|
+
rescue HTTPClient::SendTimeoutError
|
|
119
|
+
raise HTTPError.new("Request send timed out to #{host}")
|
|
120
|
+
rescue HTTPClient::ReceiveTimeoutError
|
|
121
|
+
raise HTTPError.new("Receive timed out from #{host}")
|
|
122
|
+
rescue => e
|
|
123
|
+
raise HTTPError.new("Unhandled exception within YogaPants::Connection: #{e} - #{e.message}").tap { |ex| ex.set_backtrace(e.backtrace) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def jsonify_body(string_or_hash)
|
|
127
|
+
return nil if string_or_hash.nil?
|
|
128
|
+
case string_or_hash
|
|
129
|
+
when Hash
|
|
130
|
+
JSON.dump(string_or_hash)
|
|
131
|
+
when String
|
|
132
|
+
string_or_hash
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError.new("Unrecognised body class #{string_or_hash.class}")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def http
|
|
139
|
+
@http
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def url_for(path)
|
|
143
|
+
if path[0..0] == "/"
|
|
144
|
+
"#{host}#{path}"
|
|
145
|
+
else
|
|
146
|
+
"#{host}/#{path}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'multi_json'
|
|
2
|
+
|
|
3
|
+
# FIXME: This whole thing is cancer. tire uses multi_json 1.0.3 which
|
|
4
|
+
# uses decode/encode.
|
|
5
|
+
|
|
6
|
+
module YogaPants
|
|
7
|
+
class JSON
|
|
8
|
+
def self.load(*args)
|
|
9
|
+
if MultiJson.respond_to?(:decode)
|
|
10
|
+
MultiJson.decode(*args)
|
|
11
|
+
else
|
|
12
|
+
MultiJson.load(*args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.dump(*args)
|
|
17
|
+
if MultiJson.respond_to?(:encode)
|
|
18
|
+
MultiJson.encode(*args)
|
|
19
|
+
else
|
|
20
|
+
MultiJson.dump(*args)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/yoga_pants.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
module YogaPants
|
|
4
|
+
describe "basic integration tests" do
|
|
5
|
+
subject do
|
|
6
|
+
Client.new("http://localhost:9200/")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
VCR.use_cassette('before_block') do
|
|
11
|
+
if subject.exists?("/yoga_pants_test")
|
|
12
|
+
subject.delete("/yoga_pants_test")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if subject.exists?("/yoga_pants_test_1")
|
|
16
|
+
subject.delete("/yoga_pants_test_1")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subject.post("/yoga_pants_test")
|
|
20
|
+
subject.put("/yoga_pants_test/doc/_mapping", :body => {
|
|
21
|
+
:doc => {
|
|
22
|
+
:properties => {
|
|
23
|
+
:foo => {:type => 'string'},
|
|
24
|
+
:bar => {:type => 'integer'}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
subject.post("/yoga_pants_test/_refresh")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "indexes a valid document" do
|
|
33
|
+
VCR.use_cassette('indexing') do
|
|
34
|
+
subject.post("/yoga_pants_test/doc/1", :body => {
|
|
35
|
+
:foo => 'bar'
|
|
36
|
+
})
|
|
37
|
+
subject.get("/yoga_pants_test/doc/1").should include({
|
|
38
|
+
'_index' => 'yoga_pants_test',
|
|
39
|
+
'_type' => 'doc',
|
|
40
|
+
'_id' => '1',
|
|
41
|
+
'exists' => true,
|
|
42
|
+
'_source' => {
|
|
43
|
+
'foo' => 'bar'
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "bulk operations" do
|
|
50
|
+
it 'does bulk operations just fine' do
|
|
51
|
+
VCR.use_cassette('bulk') do
|
|
52
|
+
subject.bulk("/", [
|
|
53
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 2}, {:foo => 'hello bulk'}],
|
|
54
|
+
[:index, {:_index => 'yoga_pants_test_1', :_type => 'doc2', :_id => 2}, {:foo => 'hello bulk 2'}]
|
|
55
|
+
], :refresh => true)
|
|
56
|
+
|
|
57
|
+
subject.get("/yoga_pants_test/doc/2").should include({
|
|
58
|
+
'_index' => 'yoga_pants_test',
|
|
59
|
+
'_type' => 'doc',
|
|
60
|
+
'_id' => '2',
|
|
61
|
+
'exists' => true,
|
|
62
|
+
'_source' => {
|
|
63
|
+
'foo' => 'hello bulk'
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
subject.get("/yoga_pants_test_1/doc2/2").should include({
|
|
68
|
+
'_index' => 'yoga_pants_test_1',
|
|
69
|
+
'_type' => 'doc2',
|
|
70
|
+
'_id' => '2',
|
|
71
|
+
'exists' => true,
|
|
72
|
+
'_source' => {
|
|
73
|
+
'foo' => 'hello bulk 2'
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'does not break on per-document index errors' do
|
|
80
|
+
VCR.use_cassette('bulk_document_index_error') do
|
|
81
|
+
ret = subject.bulk("/", [
|
|
82
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 1}, {:bar => 1}],
|
|
83
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 2}, {:bar => 'invalid'}],
|
|
84
|
+
])
|
|
85
|
+
errors = ret['items'].select { |operation| operation['index']['error'] }
|
|
86
|
+
errors.length.should == 1
|
|
87
|
+
errors.first["index"]["_id"].should == '2'
|
|
88
|
+
subject.exists?("/yoga_pants_test/doc/1").should be_true
|
|
89
|
+
subject.exists?("/yoga_pants_test/doc/2").should be_false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'throws an exception when ES barfs' do
|
|
94
|
+
VCR.use_cassette('bulk_error') do
|
|
95
|
+
expect do
|
|
96
|
+
subject.bulk("/", [
|
|
97
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 1}, {:bar => 1}],
|
|
98
|
+
[:index, {:_index => '', :_type => 'doc', :_id => 2}, {:bar => 'invalid'}],
|
|
99
|
+
])
|
|
100
|
+
end.to raise_error(Client::RequestError, "ElasticSearch Error: ElasticSearchException[String index out of range: 0]; nested: StringIndexOutOfBoundsException[String index out of range: 0]; ")
|
|
101
|
+
|
|
102
|
+
subject.exists?("/yoga_pants_test/doc/1").should be_false
|
|
103
|
+
subject.exists?("/yoga_pants_test/doc/2").should be_false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "multi search" do
|
|
109
|
+
it 'does bulk operations just fine' do
|
|
110
|
+
VCR.use_cassette('multi_search') do
|
|
111
|
+
result = subject.multi_search("/", [
|
|
112
|
+
[{'index' => 'yoga_pants_test', 'type' => 'doc'}, {'query' => {'match_all' => {}}}],
|
|
113
|
+
[{'index' => 'yoga_pants_test_1', 'type' => 'doc2'}, {'query' => {'match_all' => {}}}],
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
result.should have_key('responses')
|
|
117
|
+
result['responses'].size.should == 2
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "raises an exception on missing documents" do
|
|
124
|
+
VCR.use_cassette('missing') do
|
|
125
|
+
expect { subject.get("/yoga_pants_test/doc/not_exist") }.to raise_error(Client::RequestError, "Error performing HTTP request: 404 Not Found\nBody: #{'{"_index":"yoga_pants_test","_type":"doc","_id":"not_exist","exists":false}'}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises an exception on an invalid request" do
|
|
130
|
+
VCR.use_cassette('invalid_request') do
|
|
131
|
+
expect { subject.get("/this_does_not_exist") }.to raise_error(Client::RequestError, "Error performing HTTP request: 400 Bad Request\nBody: No handler found for uri [/this_does_not_exist] and method [GET]")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "raises an exception when ES returns a hash with an error object" do
|
|
136
|
+
VCR.use_cassette('error') do
|
|
137
|
+
subject.post("/yoga_pants_test/doc/1", :body => {
|
|
138
|
+
:foo => 'bar'
|
|
139
|
+
})
|
|
140
|
+
expect { subject.get("/yoga_pants_test/doc/1/_mlt?min_term_freq=invalid") }.to raise_error(Client::RequestError, "ElasticSearch Error: Failed to parse int parameter [min_term_freq] with value [invalid]")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe "network-related failures" do
|
|
147
|
+
let(:host) { "http://localhost:9200" }
|
|
148
|
+
subject do
|
|
149
|
+
Client.new(host, :connection => {:connect_timeout => 0.01})
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context "connection refused" do
|
|
153
|
+
let(:host) { "http://localhost:1" } # Unlikely to be anything running on this port
|
|
154
|
+
|
|
155
|
+
it "raises an RequestError" do
|
|
156
|
+
VCR.use_cassette('connection_refused') do
|
|
157
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, "Connection refused to http://localhost:1")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
context "connection timed out" do
|
|
163
|
+
let(:host) { "http://192.168.34.58" } # Ewww but yeah.
|
|
164
|
+
|
|
165
|
+
it "raises an RequestError" do
|
|
166
|
+
VCR.use_cassette('timed_out') do
|
|
167
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, "Connection timed out to #{host}")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe "failing over to other nodes" do
|
|
174
|
+
subject do
|
|
175
|
+
Client.new(hosts)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe "connection refused on first node" do
|
|
179
|
+
let(:hosts) { ["http://localhost:1/", "http://localhost:9200/"] }
|
|
180
|
+
it "automatically fails over" do
|
|
181
|
+
VCR.use_cassette('failover_refused') do
|
|
182
|
+
subject.exists?("/foo").should == false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "connection timed out on first node" do
|
|
188
|
+
let(:hosts) { ["http://10.13.37.3:1/", "http://localhost:9200/"] }
|
|
189
|
+
it "automatically fails over" do
|
|
190
|
+
VCR.use_cassette('failover_timeout') do
|
|
191
|
+
subject.exists?("/foo").should == false
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "no working hosts" do
|
|
197
|
+
let(:hosts) { ["http://localhost:1/", "http://localhost:2/"] }
|
|
198
|
+
it "throws an exception" do
|
|
199
|
+
VCR.use_cassette('failover_impossible') do
|
|
200
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, /Connection refused to/)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require 'vcr'
|
|
2
|
+
require 'yoga_pants'
|
|
3
|
+
|
|
4
|
+
VCR.configure do |c|
|
|
5
|
+
c.cassette_library_dir = 'fixtures/vcr_cassettes'
|
|
6
|
+
c.hook_into :webmock
|
|
7
|
+
c.allow_http_connections_when_no_cassette = true if ENV['ALLOW_NO_CASSETTE']
|
|
8
|
+
c.ignore_localhost = true if ENV['INTEGRATION']
|
|
9
|
+
end
|
data/yoga_pants.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/yoga_pants/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Jack Chen (chendo)"]
|
|
6
|
+
gem.description = %q{A super lightweight interface to ElasticSearch's HTTP REST API}
|
|
7
|
+
gem.summary = <<-TEXT.strip
|
|
8
|
+
A super lightweight interface to ElasticSearch's HTTP REST API.
|
|
9
|
+
TEXT
|
|
10
|
+
gem.homepage = "https://github.com/chendo/yoga_pants"
|
|
11
|
+
|
|
12
|
+
gem.files = `git ls-files`.split($\)
|
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
15
|
+
gem.name = "yoga_pants"
|
|
16
|
+
gem.require_paths = ["lib"]
|
|
17
|
+
gem.version = YogaPants::VERSION
|
|
18
|
+
|
|
19
|
+
gem.add_runtime_dependency 'httpclient', '2.2.5'
|
|
20
|
+
gem.add_runtime_dependency 'multi_json'
|
|
21
|
+
|
|
22
|
+
gem.add_development_dependency 'rspec'
|
|
23
|
+
gem.add_development_dependency 'vcr'
|
|
24
|
+
gem.add_development_dependency 'webmock'
|
|
25
|
+
gem.add_development_dependency 'rake'
|
|
26
|
+
end
|