heroku-vector 0.0.2

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