waistband 0.2.5 → 0.3.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/.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