waistband 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec CHANGED
@@ -1,2 +1 @@
1
- --format nested
2
- --color
1
+ --color
data/README.md CHANGED
@@ -31,17 +31,19 @@ Configuration is generally pretty simple. First, create a folder where you'll s
31
31
  ```yml
32
32
  # #{APP_DIR}/config/waistband/waistband.yml
33
33
  development:
34
+ timeout: 2
34
35
  servers:
35
36
  server1:
36
37
  host: http://localhost
37
38
  port: 9200
38
39
  ```
39
40
 
40
- You can name the servers whatever you want, and one of them is selected at random using `Array.sample` when initializing the configuration singleton. Here's an example with two servers:
41
+ You can name the servers whatever you want, and one of them is selected at random using `Array.sample`, excluding blacklisted servers, when conduction operations on the server. Here's an example with two servers:
41
42
 
42
43
  ```yml
43
44
  # #{APP_DIR}/config/waistband/waistband.yml
44
45
  development:
46
+ timeout: 2
45
47
  servers:
46
48
  server1:
47
49
  host: http://173.247.192.214
@@ -1,6 +1,7 @@
1
1
  require 'yaml'
2
2
  require 'singleton'
3
3
  require 'active_support/core_ext/hash/indifferent_access'
4
+ require 'digest/sha1'
4
5
 
5
6
  module Waistband
6
7
  class Configuration
@@ -26,26 +27,26 @@ module Waistband
26
27
  @indexes[name] ||= YAML.load_file("#{config_dir}/waistband_#{name}.yml")[@env].with_indifferent_access
27
28
  end
28
29
 
29
- def hostname
30
- "#{host}:#{port}"
31
- end
32
-
33
30
  def method_missing(method_name, *args, &block)
34
31
  return current_server[method_name] if current_server[method_name]
35
32
  return @yml_config[method_name] if @yml_config[method_name]
36
33
  super
37
34
  end
38
35
 
36
+ def servers
37
+ @servers ||= @yml_config['servers'].map do |server_name, config|
38
+ config.merge({
39
+ '_id' => Digest::SHA1.hexdigest("#{config['host']}:#{config['port']}")
40
+ })
41
+ end
42
+ end
43
+
39
44
  private
40
45
 
41
46
  def current_server
42
47
  servers.sample
43
48
  end
44
49
 
45
- def servers
46
- @servers ||= @yml_config['servers'].map {|server_name, config| config}
47
- end
48
-
49
50
  # /private
50
51
 
51
52
  end
@@ -0,0 +1,129 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
6
+ module Waistband
7
+ class Connection
8
+
9
+ class NoMoreServers < Exception; end
10
+
11
+ def initialize(options = {})
12
+ @blacklist = []
13
+ @retry_on_fail = options.fetch :retry_on_fail, true
14
+ @orderly = options.fetch :orderly, false
15
+ pick_server
16
+ end
17
+
18
+ def create!(index)
19
+ execute! 'post', relative_url_for_index(index), index_json(index)
20
+ rescue RestClient::BadRequest => ex
21
+ nil
22
+ end
23
+
24
+ def destroy!(index)
25
+ execute! 'delete', relative_url_for_index(index)
26
+ rescue RestClient::ResourceNotFound => ex
27
+ nil
28
+ end
29
+
30
+ def update_settings!(index)
31
+ execute! 'put', "#{relative_url_for_index(index)}/_settings", settings_json(index)
32
+ end
33
+
34
+ def refresh(index)
35
+ execute! 'post', "#{relative_url_for_index(index)}/_refresh", {}
36
+ end
37
+
38
+ def read(index, key)
39
+ fetched = execute! 'get', relative_url_for_key(index, key)
40
+ JSON.parse(fetched)['_source'].with_indifferent_access
41
+ rescue RestClient::ResourceNotFound => ex
42
+ nil
43
+ end
44
+
45
+ def put(index, key, data)
46
+ execute! 'put', relative_url_for_key(index, key), data.to_json
47
+ end
48
+
49
+ def delete!(index, key)
50
+ execute! 'delete', relative_url_for_key(index, key)
51
+ end
52
+
53
+ def search_url_for_index(index)
54
+ "#{url}/#{relative_url_for_index(index)}/_search"
55
+ end
56
+
57
+ private
58
+
59
+ def execute!(method_name, relative_url, data = nil)
60
+ full_url = "#{url}/#{relative_url}"
61
+
62
+ Timeout::timeout ::Waistband.config.timeout do
63
+ RestClient.send method_name, full_url, data
64
+ end
65
+ rescue Timeout::Error, Errno::EHOSTUNREACH, Errno::ECONNREFUSED => e
66
+ # something's wrong, lets blacklist this sucker
67
+ blacklist! @server
68
+ retry if @retry_on_fail
69
+ end
70
+
71
+ def relative_url_for_key(index, key)
72
+ "#{relative_url_for_index(index)}/#{index.singularize}/#{key}"
73
+ end
74
+
75
+ def relative_url_for_index(index)
76
+ "#{index_name(index)}"
77
+ end
78
+
79
+ def url
80
+ "#{@server['host']}:#{@server['port']}"
81
+ end
82
+
83
+ def index_name(index)
84
+ config(index)['name']
85
+ end
86
+
87
+ def index_json(index)
88
+ config(index).except('name', 'stringify').to_json
89
+ end
90
+
91
+ def settings_json(index)
92
+ settings = config(index)['settings']['index'].except('number_of_shards')
93
+ {index: settings}.to_json
94
+ end
95
+
96
+ def config(index)
97
+ Waistband.config.index(index)
98
+ end
99
+
100
+ def blacklist!(server)
101
+ @blacklist << server['_id'] unless @blacklist.include? server['_id']
102
+ @blacklist
103
+
104
+ pick_server
105
+ end
106
+
107
+ def pick_server
108
+ @server = next_server
109
+
110
+ unless @server
111
+ raise ::Waistband::Connection::NoMoreServers.new "No available servers remain"
112
+ end
113
+
114
+ @server
115
+ end
116
+
117
+ def next_server
118
+ return available_servers.first if @orderly
119
+ available_servers.sample
120
+ end
121
+
122
+ def available_servers
123
+ ::Waistband.config.servers.reject {|server| @blacklist.include? server['_id']}
124
+ end
125
+
126
+ # /private
127
+
128
+ end
129
+ end
@@ -1,42 +1,29 @@
1
- require 'json'
2
- require 'rest-client'
3
- require 'active_support/core_ext/string/inflections'
4
- require 'active_support/core_ext/hash/indifferent_access'
5
- require 'active_support/core_ext/hash/except'
6
-
7
1
  module Waistband
8
2
  class Index
9
3
 
10
- MAX_RETRIES = 10
11
-
12
4
  def initialize(index)
13
5
  @index = index
14
6
  @index_name = config['name']
15
7
  @stringify = config['stringify']
16
- @retries = 0
17
8
  end
18
9
 
19
10
  # create the index
20
11
  def create!
21
- RestClient.post(url, index_json)
22
- rescue RestClient::BadRequest => ex
23
- nil
12
+ connection.create! @index
24
13
  end
25
14
 
26
15
  # destroy the index
27
16
  def destroy!
28
- RestClient.delete(url)
29
- rescue RestClient::ResourceNotFound => ex
30
- nil
17
+ connection.destroy! @index
31
18
  end
32
19
 
33
20
  def update_settings!
34
- RestClient.put("#{url}/_settings", settings_json)
21
+ connection.update_settings! @index
35
22
  end
36
23
 
37
24
  # refresh the index
38
25
  def refresh
39
- RestClient.post("#{url}/_refresh", {})
26
+ connection.refresh @index
40
27
  end
41
28
 
42
29
  def store!(key, data)
@@ -44,59 +31,45 @@ module Waistband
44
31
  if @stringify
45
32
  original_data = data
46
33
 
47
- if data.is_a?(Array)
48
- data = Waistband::StringifiedArray.new(data)
49
- elsif data.is_a?(Hash)
50
- data = Waistband::StringifiedHash.new_from(data)
34
+ if data.is_a? Array
35
+ data = ::Waistband::StringifiedArray.new data
36
+ elsif data.is_a? Hash
37
+ data = ::Waistband::StringifiedHash.new_from data
51
38
  end
52
39
 
53
- data = data.stringify_all if data.respond_to?(:stringify_all)
40
+ data = data.stringify_all if data.respond_to? :stringify_all
54
41
  end
55
42
 
56
- result = RestClient.put(url_for_key(key), data.to_json)
43
+ result = connection.put @index, key, data
57
44
  data = original_data if @stringify
58
45
 
59
46
  result
60
47
  end
61
48
 
62
49
  def delete!(key)
63
- RestClient.delete(url_for_key(key))
50
+ connection.delete! @index, key
64
51
  end
65
52
 
66
53
  def read(key)
67
- fetched = RestClient.get(url_for_key(key))
68
- JSON.parse(fetched)['_source'].with_indifferent_access
69
- rescue RestClient::ResourceNotFound => ex
70
- nil
54
+ connection.read @index, key
71
55
  end
72
56
 
73
57
  def query(term, options = {})
74
- Waistband::Query.new(@index_name, term, options)
58
+ ::Waistband::Query.new @index, term, options
75
59
  end
76
60
 
77
- private
78
-
79
- def url_for_key(key)
80
- "#{url}/#{@index.singularize}/#{key}"
81
- end
61
+ def search_url
62
+ connection.search_url_for_index @index
63
+ end
82
64
 
83
- def settings_json
84
- @settings_json ||= begin
85
- settings = config['settings']['index'].except('number_of_shards')
86
- {index: settings}.to_json
87
- end
88
- end
65
+ private
89
66
 
90
- def index_json
91
- @index_json ||= config.except('name', 'stringify').to_json
67
+ def connection
68
+ ::Waistband::Connection.new
92
69
  end
93
70
 
94
71
  def config
95
- @config ||= Waistband.config.index(@index)
96
- end
97
-
98
- def url
99
- "#{Waistband.config.hostname}/#{@index_name}"
72
+ Waistband.config.index @index
100
73
  end
101
74
 
102
75
  # /private
@@ -99,12 +99,16 @@ module Waistband
99
99
 
100
100
  private
101
101
 
102
- def execute!
103
- @executed ||= JSON.parse(RestClient.post(url, to_hash.to_json))
102
+ def url
103
+ index.search_url
104
104
  end
105
105
 
106
- def url
107
- "#{Waistband.config.hostname}/#{@index}/_search"
106
+ def index
107
+ Waistband::Index.new(@index)
108
+ end
109
+
110
+ def execute!
111
+ @executed ||= JSON.parse(RestClient.post(url, to_hash.to_json))
108
112
  end
109
113
 
110
114
  def to_hash
@@ -1,3 +1,3 @@
1
1
  module Waistband
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/waistband.rb CHANGED
@@ -3,6 +3,7 @@ require "waistband/version"
3
3
  module Waistband
4
4
 
5
5
  autoload :Configuration, "waistband/configuration"
6
+ autoload :Connection, "waistband/connection"
6
7
  autoload :StringifiedArray, "waistband/stringified_array"
7
8
  autoload :StringifiedHash, "waistband/stringified_hash"
8
9
  autoload :QueryResult, "waistband/query_result"
@@ -1,10 +1,11 @@
1
1
  development: &DEV
2
+ timeout: 2
2
3
  servers:
3
4
  server1:
4
5
  host: http://localhost
5
6
  port: 9200
6
7
  server2:
7
- host: http://localhost
8
+ host: http://127.0.0.1
8
9
  port: 9200
9
10
 
10
11
  test:
@@ -5,8 +5,9 @@ describe Waistband::Configuration do
5
5
  let(:config) { Waistband.config }
6
6
 
7
7
  it "loads config yml" do
8
- config.host.should eql 'http://localhost'
8
+ config.host.should match /http\:\/\//
9
9
  config.port.should eql 9200
10
+ config.timeout.should eql 2
10
11
  end
11
12
 
12
13
  it "loads indexes config" do
@@ -21,4 +22,25 @@ describe Waistband::Configuration do
21
22
  config.index('events')['settings']['index']['number_of_shards'].should eql 4
22
23
  end
23
24
 
25
+ describe '#servers' do
26
+
27
+ it "returns array of all available servers' configs" do
28
+ config.servers.should be_an Array
29
+ config.servers.size.should eql 2
30
+
31
+ config.servers.each_with_index do |server, i|
32
+ server['host'].should match /http\:\/\//
33
+ server['port'].should eql 9200
34
+
35
+ server['_id'].should be_present
36
+ server['_id'].length.should eql 40
37
+ end
38
+ end
39
+
40
+ it "servers ids should be unique" do
41
+ config.servers[0]['_id'].should_not eql config.servers[1]['_id']
42
+ end
43
+
44
+ end
45
+
24
46
  end
@@ -0,0 +1,225 @@
1
+ require 'spec_helper'
2
+
3
+ describe Waistband::Connection do
4
+
5
+ let(:connection) { Waistband::Connection.new }
6
+
7
+ def blacklist_server!
8
+ connection.send(:blacklist!, Waistband.config.servers.first)
9
+ end
10
+
11
+ it "constructs the settings json" do
12
+ connection.send(:settings_json, 'events').should eql '{"index":{"number_of_replicas":1}}'
13
+ end
14
+
15
+ it "constructs the index json" do
16
+ connection.send(:index_json, 'events').should eql '{"settings":{"index":{"number_of_shards":4,"number_of_replicas":1}},"mappings":{"event":{"_source":{"includes":["*"]}}}}'
17
+ end
18
+
19
+ describe '#execute!' do
20
+
21
+ it "wraps directly to rest client" do
22
+ connection = Waistband::Connection.new(orderly: true)
23
+
24
+ RestClient.should_receive(:get).with('http://localhost:9200/somekey', nil).once
25
+ connection.send(:execute!, 'get', 'somekey')
26
+ end
27
+
28
+ describe 'failures' do
29
+
30
+ [Timeout::Error, Errno::EHOSTUNREACH, Errno::ECONNREFUSED].each do |exception|
31
+ it "blacklists the server when #{exception}" do
32
+ connection = Waistband::Connection.new(retry_on_fail: false)
33
+
34
+ RestClient.should_receive(:get).with(kind_of(String), nil).and_raise exception
35
+ connection.send(:execute!, 'get', 'somekey')
36
+
37
+ connection.send(:available_servers).size.should eql 1
38
+ end
39
+ end
40
+
41
+ it "blacklists correctly when server is not responding" do
42
+ ::Waistband.config.stub(:servers).and_return(
43
+ [
44
+ {
45
+ host: "http://localhost",
46
+ port: 9123,
47
+ _id: "567890a5ce74182e5cd123e299993ab510c56123"
48
+ }.with_indifferent_access,
49
+ {
50
+ host: "http://localhost",
51
+ port: 9200,
52
+ _id: "282f32a5ce74182e5cd628e298b93ab510c5660c"
53
+ }.with_indifferent_access
54
+ ]
55
+ )
56
+
57
+ connection = Waistband::Connection.new(orderly: true)
58
+ expect { connection.refresh('events') }.to_not raise_error
59
+ end
60
+
61
+ it "keeps retrying till out of servers when retry_on_fail is true" do
62
+ RestClient.should_receive(:get).with('http://localhost:9200/somekey', nil).once.and_raise(Timeout::Error)
63
+ RestClient.should_receive(:get).with('http://127.0.0.1:9200/somekey', nil).once.and_raise(Timeout::Error)
64
+
65
+ expect {
66
+ connection.send(:execute!, 'get', 'somekey')
67
+ }.to raise_error(
68
+ Waistband::Connection::NoMoreServers,
69
+ "No available servers remain"
70
+ )
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ describe '#relative_url_for_key' do
78
+
79
+ it "returns the relative url for a key" do
80
+ url = connection.send(:relative_url_for_key, 'search', 'key123')
81
+ url.should match /^search_test\/search\/key123$/
82
+
83
+ url = connection.send(:relative_url_for_key, 'events', '9986')
84
+ url.should match /^events_test\/event\/9986$/
85
+ end
86
+
87
+ end
88
+
89
+ describe '#relative_url_for_index' do
90
+
91
+ it "returns the url for an index" do
92
+ url = connection.send(:relative_url_for_index, 'search')
93
+ url.should match /^search_test$/
94
+
95
+ url = connection.send(:relative_url_for_index, 'events')
96
+ url.should match /^events_test$/
97
+ end
98
+
99
+ end
100
+
101
+ describe '#url' do
102
+
103
+ it "returns url string for the selected server" do
104
+ url = connection.send(:url)
105
+ url.should match /^http\:\/\//
106
+ url.should match /\:9200$/
107
+ end
108
+
109
+ end
110
+
111
+ describe '#pick_server' do
112
+
113
+ it "randomly picks a server" do
114
+ server = connection.send :pick_server
115
+ server['host'].should match /http\:\/\//
116
+ server['port'].should eql 9200
117
+ end
118
+
119
+ it "never picks blacklisted servers" do
120
+ blacklist_server!
121
+
122
+ 200.times do
123
+ server = connection.send :pick_server
124
+
125
+ server['host'].should eql 'http://127.0.0.1'
126
+ end
127
+ end
128
+
129
+ it "blows up when no more servers remain" do
130
+ blacklist_server!
131
+
132
+ expect {
133
+ connection.send(:blacklist!, Waistband.config.servers.last)
134
+ }.to raise_error(
135
+ Waistband::Connection::NoMoreServers,
136
+ "No available servers remain"
137
+ )
138
+ end
139
+
140
+ end
141
+
142
+ describe '#blacklist!' do
143
+
144
+ it "blacklists a server" do
145
+ connection.instance_variable_get('@blacklist').should be_empty
146
+
147
+ blacklist_server!
148
+
149
+ connection.instance_variable_get('@blacklist').size.should eql 1
150
+ connection.instance_variable_get('@blacklist').first.should eql Waistband.config.servers.first['_id']
151
+ end
152
+
153
+ it "doesn't keep duplicate servers" do
154
+ connection.instance_variable_get('@blacklist').should be_empty
155
+
156
+ blacklist_server!
157
+
158
+ connection.instance_variable_get('@blacklist').size.should eql 1
159
+
160
+ blacklist_server!
161
+
162
+ connection.instance_variable_get('@blacklist').size.should eql 1
163
+ end
164
+
165
+ end
166
+
167
+ describe '#available_servers' do
168
+
169
+ it "returns an array of servers" do
170
+ connection.send(:available_servers).should be_an Array
171
+ connection.send(:available_servers).size.should eql 2
172
+ end
173
+
174
+ it "doesn't include blacklisted servers" do
175
+ blacklist_server!
176
+
177
+ connection.send(:available_servers).should be_an Array
178
+ connection.send(:available_servers).size.should eql 1
179
+
180
+ ids = connection.send(:available_servers).map{|s| s['_id']}
181
+ ids.should include Waistband.config.servers.last['_id']
182
+ ids.should_not include Waistband.config.servers.first['_id']
183
+ end
184
+
185
+ end
186
+
187
+ describe "storing" do
188
+
189
+ let(:index) { ::Waistband::Index.new('events') }
190
+ let(:index2) { ::Waistband::Index.new('search') }
191
+ let(:attrs) { {'ok' => {'yeah' => true}} }
192
+
193
+ before { IndexHelper.prepare! }
194
+
195
+ it "stores data" do
196
+ connection.put('events', '__test_write', {'ok' => 'yeah'})
197
+ index.read('__test_write').should eql({'ok' => 'yeah'})
198
+ end
199
+
200
+ it "data is indirectly accessible" do
201
+ connection.put('events', '__test_not_string', attrs)
202
+ index.read('__test_not_string')[:ok][:yeah].should eql true
203
+ end
204
+
205
+ it "deletes data" do
206
+ connection.put('events', '__test_write', attrs)
207
+ connection.delete!('events', '__test_write')
208
+ index.read('__test_write').should be_nil
209
+ end
210
+
211
+ it "returns nil on 404" do
212
+ index.read('__not_here').should be_nil
213
+ end
214
+
215
+ it "doesn't mix data between two indexes" do
216
+ connection.put('events', '__test_write', {'data' => 'index_1'})
217
+ connection.put('search', '__test_write', {'data' => 'index_2'})
218
+
219
+ index.read('__test_write').should eql({'data' => 'index_1'})
220
+ index2.read('__test_write').should eql({'data' => 'index_2'})
221
+ end
222
+
223
+ end
224
+
225
+ end
@@ -9,7 +9,6 @@ describe Waistband::Index do
9
9
  it "initializes values" do
10
10
  index.instance_variable_get('@index_name').should eql 'events_test'
11
11
  index.instance_variable_get('@stringify').should eql true
12
- index.instance_variable_get('@retries').should eql 0
13
12
  end
14
13
 
15
14
  it "creates the index" do
@@ -32,14 +31,6 @@ describe Waistband::Index do
32
31
  response['ok'].should be_true
33
32
  end
34
33
 
35
- it "constructs the settings json" do
36
- index.send(:settings_json).should eql '{"index":{"number_of_replicas":1}}'
37
- end
38
-
39
- it "constructs the index json" do
40
- index.send(:index_json).should eql '{"settings":{"index":{"number_of_shards":4,"number_of_replicas":1}},"mappings":{"event":{"_source":{"includes":["*"]}}}}'
41
- end
42
-
43
34
  it "proxies to a query" do
44
35
  index.query('shopping').should be_a Waistband::Query
45
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waistband
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-28 00:00:00.000000000 Z
12
+ date: 2013-09-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -108,6 +108,7 @@ files:
108
108
  - Rakefile
109
109
  - lib/waistband.rb
110
110
  - lib/waistband/configuration.rb
111
+ - lib/waistband/connection.rb
111
112
  - lib/waistband/index.rb
112
113
  - lib/waistband/model.rb
113
114
  - lib/waistband/query.rb
@@ -120,6 +121,7 @@ files:
120
121
  - spec/config/waistband/waistband_events.yml
121
122
  - spec/config/waistband/waistband_search.yml
122
123
  - spec/lib/configuration_spec.rb
124
+ - spec/lib/connection_spec.rb
123
125
  - spec/lib/index_spec.rb
124
126
  - spec/lib/model_spec.rb
125
127
  - spec/lib/query_result_spec.rb
@@ -159,6 +161,7 @@ test_files:
159
161
  - spec/config/waistband/waistband_events.yml
160
162
  - spec/config/waistband/waistband_search.yml
161
163
  - spec/lib/configuration_spec.rb
164
+ - spec/lib/connection_spec.rb
162
165
  - spec/lib/index_spec.rb
163
166
  - spec/lib/model_spec.rb
164
167
  - spec/lib/query_result_spec.rb