heroku-vector 0.0.2

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.
@@ -0,0 +1,192 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://rpm.newrelic.com/accounts.xml
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept:
11
+ - application/xml
12
+ X-Api-Key:
13
+ - api_key
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ User-Agent:
17
+ - Ruby
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Server:
24
+ - nginx
25
+ Date:
26
+ - Sun, 07 Sep 2014 06:32:38 GMT
27
+ Content-Type:
28
+ - application/xml; charset=utf-8
29
+ Transfer-Encoding:
30
+ - chunked
31
+ Connection:
32
+ - keep-alive
33
+ Status:
34
+ - 200 OK
35
+ X-Newrelic-Api-Version:
36
+ - v1
37
+ Etag:
38
+ - '"6421a9efd042eb1f557d443b6010b3b9"'
39
+ Cache-Control:
40
+ - max-age=0, private, must-revalidate
41
+ X-Ua-Compatible:
42
+ - IE=Edge,chrome=1
43
+ X-Runtime:
44
+ - '0.018450'
45
+ body:
46
+ encoding: UTF-8
47
+ string: |
48
+ <?xml version="1.0" encoding="UTF-8"?>
49
+ <accounts type="array">
50
+ <account>
51
+ <allow-rails-core>false</allow-rails-core>
52
+ <api-key>api_key</api-key>
53
+ <data-access-key>8fe689a3c6e5b099a28551733333c2acaeb2a80136c9357</data-access-key>
54
+ <id type="integer">1</id>
55
+ <license-key>adb4b49abefe681dce6d228d764082f1b36c9357</license-key>
56
+ <name>polar-rails-staging Heroku</name>
57
+ <partner-external-identifier nil="true"></partner-external-identifier>
58
+ <phone-number nil="true"></phone-number>
59
+ <event-feed-uri>/account_feeds/8fe689a3c6e5b099a28551733333c2acaeb2a80136c9357/events.rss</event-feed-uri>
60
+ <primary-admin>
61
+ <email>cole.jeff.services@gmail.com</email>
62
+ <first-name nil="true"></first-name>
63
+ <last-name nil="true"></last-name>
64
+ <state>active</state>
65
+ </primary-admin>
66
+ <subscription>
67
+ <annual-renewal-on nil="true"></annual-renewal-on>
68
+ <expires-on nil="true"></expires-on>
69
+ <number-of-hosts>unlimited</number-of-hosts>
70
+ <starts-on>2013-11-02</starts-on>
71
+ <state>free</state>
72
+ <product-name>Standard</product-name>
73
+ </subscription>
74
+ </account>
75
+ </accounts>
76
+ http_version:
77
+ recorded_at: Sun, 07 Sep 2014 06:32:37 GMT
78
+ - request:
79
+ method: get
80
+ uri: http://rpm.newrelic.com/accounts/1/applications.xml
81
+ body:
82
+ encoding: US-ASCII
83
+ string: ''
84
+ headers:
85
+ Accept:
86
+ - application/xml
87
+ X-Api-Key:
88
+ - api_key
89
+ Accept-Encoding:
90
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
91
+ User-Agent:
92
+ - Ruby
93
+ response:
94
+ status:
95
+ code: 200
96
+ message: OK
97
+ headers:
98
+ Server:
99
+ - nginx
100
+ Date:
101
+ - Sun, 07 Sep 2014 06:32:39 GMT
102
+ Content-Type:
103
+ - application/xml; charset=utf-8
104
+ Transfer-Encoding:
105
+ - chunked
106
+ Connection:
107
+ - keep-alive
108
+ Status:
109
+ - 200 OK
110
+ X-Newrelic-Api-Version:
111
+ - v1
112
+ Etag:
113
+ - '"a2f87f24fee08dc231c1275662707617"'
114
+ Cache-Control:
115
+ - max-age=0, private, must-revalidate
116
+ X-Ua-Compatible:
117
+ - IE=Edge,chrome=1
118
+ X-Runtime:
119
+ - '0.027413'
120
+ body:
121
+ encoding: UTF-8
122
+ string: |
123
+ <?xml version="1.0" encoding="UTF-8"?>
124
+ <applications type="array">
125
+ <application>
126
+ <id type="integer">2</id>
127
+ <name>polar-rails-staging</name>
128
+ <overview-url>https://rpm.newrelic.com/accounts/1/applications/2</overview-url>
129
+ <servers-url>https://rpm.newrelic.com/api/v1/accounts/1/applications/2/servers</servers-url>
130
+ </application>
131
+ </applications>
132
+ http_version:
133
+ recorded_at: Sun, 07 Sep 2014 06:32:38 GMT
134
+ - request:
135
+ method: get
136
+ uri: http://rpm.newrelic.com/accounts/1/applications/2/threshold_values.xml
137
+ body:
138
+ encoding: US-ASCII
139
+ string: ''
140
+ headers:
141
+ Accept:
142
+ - application/xml
143
+ X-Api-Key:
144
+ - 68067fd6b945d14883d2b9fdc0e0d85954baab4036c9357
145
+ Accept-Encoding:
146
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
147
+ User-Agent:
148
+ - Ruby
149
+ response:
150
+ status:
151
+ code: 200
152
+ message: OK
153
+ headers:
154
+ Server:
155
+ - nginx
156
+ Date:
157
+ - Sun, 07 Sep 2014 06:32:39 GMT
158
+ Content-Type:
159
+ - application/xml; charset=utf-8
160
+ Transfer-Encoding:
161
+ - chunked
162
+ Connection:
163
+ - keep-alive
164
+ Status:
165
+ - 200 OK
166
+ X-Newrelic-Api-Version:
167
+ - v1
168
+ Etag:
169
+ - '"b3095ce6278aac5a4e88d824c71f1c1e"'
170
+ Cache-Control:
171
+ - max-age=0, private, must-revalidate
172
+ X-Ua-Compatible:
173
+ - IE=Edge,chrome=1
174
+ X-Runtime:
175
+ - '0.033636'
176
+ body:
177
+ encoding: UTF-8
178
+ string: |
179
+ <?xml version="1.0" encoding="UTF-8"?>
180
+ <threshold-values type="array">
181
+ <threshold_value name="Apdex" metric_value="1.0" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="1 [0.5]*"/>
182
+ <threshold_value name="Error Rate" metric_value="0" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="0%"/>
183
+ <threshold_value name="Throughput" metric_value="123" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="123 rpm"/>
184
+ <threshold_value name="Errors" metric_value="0" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="0 epm"/>
185
+ <threshold_value name="Response Time" metric_value="0" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="0 ms"/>
186
+ <threshold_value name="DB" metric_value="0" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="0%"/>
187
+ <threshold_value name="CPU" metric_value="0.38" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="0.38%"/>
188
+ <threshold_value name="Memory" metric_value="1490" threshold_value="1" begin_time="2014-09-07 06:27:57" end_time="2014-09-07 06:30:57" formatted_metric_value="1,490 MB"/>
189
+ </threshold-values>
190
+ http_version:
191
+ recorded_at: Sun, 07 Sep 2014 06:32:38 GMT
192
+ recorded_with: VCR 2.9.2
@@ -0,0 +1,240 @@
1
+ require File.absolute_path File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ describe HerokuVector::DynoScaler do
4
+ TestSource = Struct.new(:sample)
5
+
6
+ let(:dyno_name) { 'web' }
7
+ let(:mock_heroku) { mock('heroku') }
8
+ let(:source) { HerokuVector::Source::NewRelic }
9
+
10
+ let(:options) do
11
+ {
12
+ :source => source,
13
+ :period => 60,
14
+ :sample_size => 10,
15
+ :min_dynos => 1,
16
+ :max_dynos => 4,
17
+ :min_value => 3000,
18
+ :max_value => 5000,
19
+ :engine => mock_heroku
20
+ }
21
+ end
22
+
23
+ describe '#initialize' do
24
+ it 'should assign values' do
25
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
26
+
27
+ assert_equal dyno_name, scaler.name
28
+ assert_equal options[:source], scaler.source.class
29
+ assert_equal options[:period], scaler.period
30
+ assert_equal options[:sample_size], scaler.sampler.capacity
31
+ assert_equal options[:min_dynos], scaler.min_dynos
32
+ assert_equal options[:max_dynos], scaler.max_dynos
33
+ assert_equal options[:min_value], scaler.min_value
34
+ assert_equal options[:max_value], scaler.max_value
35
+ assert_equal mock_heroku, scaler.engine
36
+ assert scaler.sampler
37
+ end
38
+
39
+ it 'should set default values' do
40
+ scaler = HerokuVector::DynoScaler.new(dyno_name, {
41
+ :min_value => 100,
42
+ :max_value => 200,
43
+ :source => source
44
+ })
45
+
46
+ assert_equal 60, scaler.period
47
+ assert_equal 5, scaler.sampler.capacity
48
+ assert_equal 2, scaler.min_dynos
49
+ assert_equal 10, scaler.max_dynos
50
+ assert_equal 1, scaler.scale_up_by
51
+ assert_equal 1, scaler.scale_down_by
52
+
53
+ assert scaler.sampler
54
+ assert_equal HerokuVector::Engine::Heroku, scaler.engine.class
55
+ end
56
+
57
+ it 'should require min/max values' do
58
+ assert_raises(RuntimeError) do
59
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options.merge(:min_value => nil))
60
+ end
61
+ assert_raises(RuntimeError) do
62
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options.merge(:max_value => nil))
63
+ end
64
+ end
65
+
66
+ it 'should instantiate source class instance' do
67
+ clazz = 'NewRelic'
68
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options.merge(:source => clazz) )
69
+ assert_equal "HerokuVector::Source::#{clazz}", scaler.source.class.name
70
+
71
+ assert_raises(RuntimeError) do
72
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options.merge(:source => 'invalid_class'))
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#run' do
78
+ let (:scaler) do
79
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
80
+ scaler.source = TestSource.new(1)
81
+ scaler
82
+ end
83
+
84
+ it 'should collect a new sample' do
85
+ scaler.expects(:collect_sample)
86
+
87
+ scaler.run
88
+ end
89
+
90
+ it 'should not evaluate scale until enough samples' do
91
+ scaler.stubs(:enough_samples? => false)
92
+ scaler.expects(:evaluate_and_scale).never
93
+
94
+ scaler.run
95
+ end
96
+
97
+ it 'should not evaluate scale if scaled recently' do
98
+ scaler.stubs(:enough_samples? => true)
99
+ scaler.stubs(:scaling_too_soon? => true)
100
+
101
+ scaler.expects(:evaluate_and_scale).never
102
+
103
+ scaler.run
104
+ end
105
+
106
+ it 'should evaluate_and_scale w/ enough samples' do
107
+ scaler.stubs(:enough_samples? => true)
108
+ scaler.stubs(:scaling_too_soon? => false)
109
+
110
+ scaler.expects(:evaluate_and_scale)
111
+
112
+ scaler.run
113
+ end
114
+ end
115
+
116
+ describe '#reset' do
117
+ let (:scaler) do
118
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
119
+ scaler.source = TestSource.new(1)
120
+ scaler
121
+ end
122
+
123
+ it 'should reset sampler' do
124
+ assert_equal 0, scaler.sampler.size
125
+ scaler.collect_sample
126
+ assert_equal 1, scaler.sampler.size
127
+
128
+ scaler.reset
129
+
130
+ assert_equal 0, scaler.sampler.size
131
+ end
132
+
133
+ it 'should reset last_scale_time' do
134
+ assert_equal nil, scaler.last_scale_time
135
+ scaler.record_last_scale_event
136
+ assert scaler.last_scale_time
137
+
138
+ scaler.reset
139
+
140
+ assert_equal nil, scaler.last_scale_time
141
+ end
142
+ end
143
+
144
+ describe '#evaluate_and_scale' do
145
+ let (:scaler) do
146
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
147
+ scaler.source = TestSource.new(1)
148
+ scaler.reset
149
+ scaler
150
+ end
151
+
152
+ it 'should not scale dynos inside min/max range' do
153
+ scaler.stubs(:current_size => 1)
154
+ scaler.source = TestSource.new(scaler.min_value + 1)
155
+ scaler.collect_sample
156
+
157
+ scaler.expects(:scale_dynos).never
158
+
159
+ scaler.evaluate_and_scale
160
+ end
161
+
162
+ it 'should scale down dynos below min_value' do
163
+ scaler.stubs(:current_size => 2)
164
+ scaler.source = TestSource.new(scaler.min_value + 1)
165
+ scaler.collect_sample
166
+
167
+ scaler.expects(:scale_dynos).with(2, 1)
168
+
169
+ scaler.evaluate_and_scale
170
+ end
171
+
172
+ it 'should scale up dynos above max_value' do
173
+ scaler.stubs(:current_size => 1)
174
+ scaler.source = TestSource.new(scaler.max_value + 1)
175
+ scaler.collect_sample
176
+
177
+ scaler.expects(:scale_dynos).with(1, 2)
178
+
179
+ scaler.evaluate_and_scale
180
+ end
181
+ end
182
+
183
+ describe '#scale_dynos' do
184
+ let (:scaler) do
185
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
186
+ end
187
+
188
+ it 'should not scale if amount is the same' do
189
+ mock_heroku.expects(:scale_dynos).never
190
+
191
+ scaler.scale_dynos(1, 1)
192
+ scaler.scale_dynos(2, 2)
193
+ scaler.scale_dynos(3, 3)
194
+ scaler.scale_dynos(4, 4)
195
+ end
196
+
197
+ it 'should not scale below min_dynos' do
198
+ mock_heroku.expects(:scale_dynos).never
199
+
200
+ scaler.scale_dynos(1, 0)
201
+ end
202
+
203
+ it 'should not scale above max_dynos' do
204
+ mock_heroku.expects(:scale_dynos).never
205
+
206
+ scaler.scale_dynos(4, 5)
207
+ end
208
+
209
+ it 'should scale within min/max' do
210
+ mock_heroku.expects(:scale_dynos).with(scaler.name, 2)
211
+
212
+ scaler.scale_dynos(1, 2)
213
+ end
214
+ end
215
+
216
+ describe '#scaling_too_soon?' do
217
+ let (:scaler) do
218
+ scaler = HerokuVector::DynoScaler.new(dyno_name, options)
219
+ end
220
+
221
+ it 'should not be too soon w/out last_scale_time' do
222
+ assert_equal nil, scaler.last_scale_time
223
+ assert_equal false, scaler.scaling_too_soon?
224
+ end
225
+
226
+ it 'should be too soon near last_scale_time' do
227
+ scaler.last_scale_time = Time.now
228
+
229
+ assert_equal true, scaler.scaling_too_soon?
230
+ end
231
+
232
+ it 'should not be too soon after last_scale_time' do
233
+ threshold = HerokuVector::DynoScaler::MIN_SCALE_TIME_DELTA_SEC
234
+ scaler.last_scale_time = Time.now - threshold - 1
235
+
236
+ assert_equal false, scaler.scaling_too_soon?
237
+ end
238
+ end
239
+
240
+ end
@@ -0,0 +1,91 @@
1
+ require File.absolute_path File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ describe HerokuVector::Engine::Heroku do
4
+ let(:heroku_app_name) { 'app_name' }
5
+ let(:engine) { HerokuVector::Engine::Heroku.new }
6
+ before { HerokuVector.stubs(:heroku_app_name => heroku_app_name) }
7
+
8
+ describe '#initialize' do
9
+ it 'should set app name from config' do
10
+ assert_equal heroku_app_name, engine.app
11
+ end
12
+
13
+ it 'should instantiate a Heroku API client' do
14
+ assert engine.heroku
15
+ end
16
+ end
17
+
18
+ describe '#get_dyno_types' do
19
+ it 'should load dyno definitions from Heroku API' do
20
+ VCR.use_cassette('heroku_dyno_types') do
21
+ dyno_types = engine.get_dyno_types
22
+ dynos_by_name = dyno_types.index_by {|dyno| dyno["name"] }
23
+ assert_equal 1, dynos_by_name['web']['quantity']
24
+ expected_command = "bin/nginx_wrapper bundle exec rainbows -c ./config/rainbows.rb"
25
+ assert_equal expected_command, dynos_by_name['web']['command']
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#scale_dynos' do
31
+ it 'should call Heroku API and issue command' do
32
+ VCR.use_cassette('heroku_scale_dynos') do
33
+ engine.scale_dynos('web', 1)
34
+
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#get_dynos_by_name' do
40
+ it 'should index dyno array by name' do
41
+ dynos = [{ 'name' => 'web', 'quantity' => 1 }]
42
+ engine.expects(:get_dyno_types => dynos)
43
+
44
+ dynos_by_name = engine.get_dynos_by_name
45
+ assert_equal 1, dynos_by_name['web']['quantity']
46
+ end
47
+ end
48
+
49
+ describe '#get_dynos_by_name_cached' do
50
+ it 'should cache calls to get_dynos_by_name' do
51
+ value = 'dynos'
52
+ engine.expects(:get_dynos_by_name => value).once
53
+
54
+ time = Time.now
55
+ Timecop.freeze(time) do
56
+ assert_equal value, engine.get_dynos_by_name_cached
57
+ assert_equal value, engine.get_dynos_by_name_cached
58
+ cache_entry = engine.instance_variable_get(:@dynos_by_name)
59
+ assert cache_entry
60
+ assert_equal value, cache_entry.data
61
+ assert_equal time + 60, cache_entry.expires_at
62
+ end
63
+ end
64
+
65
+ it 'should get new value when cached value expired' do
66
+ value = 'dynos'
67
+ engine.expects(:get_dynos_by_name => value).twice
68
+
69
+ time = Time.now
70
+ Timecop.freeze(time) do
71
+ assert_equal value, engine.get_dynos_by_name_cached
72
+ assert_equal value, engine.get_dynos_by_name_cached
73
+ end
74
+ Timecop.freeze(time+61) do
75
+ assert_equal value, engine.get_dynos_by_name_cached
76
+ end
77
+ end
78
+
79
+ describe '#count_for_dyno_name' do
80
+ it 'should lookup dyno count' do
81
+ dynos = {
82
+ 'web' => { 'name' => 'web', 'quantity' => 1 }
83
+ }
84
+ engine.stubs(:get_dynos_by_name_cached => dynos)
85
+
86
+ assert_equal 0, engine.count_for_dyno_name('unknown'), 'should return 0 for unknown dyno'
87
+ assert_equal 1, engine.count_for_dyno_name('web'), 'should return dyno quantity from dynos set'
88
+ end
89
+ end
90
+ end
91
+ end