dyno_scaler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ require "spec_helper"
2
+
3
+ describe DynoScaler::Configuration do
4
+ subject(:config) { described_class.new }
5
+
6
+ it "defaults max_workers to 1" do
7
+ config.max_workers.should eq(1)
8
+ end
9
+
10
+ it "defaults min_workers to 0" do
11
+ config.min_workers.should eq(0)
12
+ end
13
+
14
+ it "defaults to not enabled" do
15
+ config.enabled.should be_false
16
+ end
17
+
18
+ it "defaults job_worker_ratio to { 1 => 1, 2 => 25, 3 => 50, 4 => 75, 5 => 100 }" do
19
+ config.job_worker_ratio.should eq({
20
+ 1 => 1,
21
+ 2 => 25,
22
+ 3 => 50,
23
+ 4 => 75,
24
+ 5 => 100
25
+ })
26
+ end
27
+
28
+ it "defaults application to nil" do
29
+ config.application.should be_nil
30
+ end
31
+
32
+ context "when HEROKU_API_KEY environment variable is configured" do
33
+ before { ENV['HEROKU_API_KEY'] = 'some-api-key' }
34
+ after { ENV['HEROKU_API_KEY'] = nil }
35
+
36
+ it "defaults to enabled" do
37
+ config.should be_enabled
38
+ end
39
+ end
40
+
41
+ context "when HEROKU_APP environment variable is configured" do
42
+ before { ENV['HEROKU_APP'] = 'my-app-on-heroku' }
43
+ after { ENV['HEROKU_APP'] = nil }
44
+
45
+ it "defaults application to the value of HEROKU_APP" do
46
+ config.application.should eq(ENV['HEROKU_APP'])
47
+ end
48
+ end
49
+
50
+ describe "async" do
51
+ it "defaults to false" do
52
+ config.async.should be_false
53
+ end
54
+
55
+ context "when set to true" do
56
+ before { config.async = true }
57
+
58
+ it "configures a GirlFriday-callable" do
59
+ config.async.should respond_to(:call)
60
+ end
61
+ end
62
+
63
+ context "when configured with a block" do
64
+ before do
65
+ config.async { :ok }
66
+ end
67
+
68
+ it "configures the callable" do
69
+ config.async.call.should eq(:ok)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe DynoScaler::Heroku do
4
+ let(:application) { 'my-application-on-heroku' }
5
+ subject(:heroku_client) { DynoScaler::Heroku.new application }
6
+
7
+ let(:heroku_api) { mock(::Heroku::API, :post_ps_scale => false) }
8
+
9
+ before do
10
+ ::Heroku::API.stub!(:new).and_return(heroku_api)
11
+ end
12
+
13
+ describe "scaling workers" do
14
+ let(:quantity) { 2 }
15
+
16
+ it "scales workers of the application to the given number of workers" do
17
+ heroku_api.should_receive(:post_ps_scale).with(application, 'worker', quantity)
18
+ heroku_client.scale_workers(quantity)
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,349 @@
1
+ require "spec_helper"
2
+
3
+ describe DynoScaler::Manager do
4
+ let(:config) { DynoScaler.configuration }
5
+
6
+ subject(:manager) { DynoScaler::Manager.new }
7
+
8
+ let(:heroku) { mock(DynoScaler::Heroku, scale_workers: false) }
9
+
10
+ let(:workers) { 0 }
11
+ let(:pending_jobs) { 0 }
12
+ let(:running_jobs) { 0 }
13
+
14
+ let(:options) do
15
+ {
16
+ workers: workers,
17
+ pending: pending_jobs,
18
+ working: running_jobs
19
+ }
20
+ end
21
+
22
+ before do
23
+ DynoScaler.reset!
24
+ DynoScaler.configuration.logger = Logger.new(StringIO.new)
25
+
26
+ config.max_workers = 5
27
+ config.application = 'my-app'
28
+ config.enabled = true
29
+
30
+ DynoScaler::Heroku.stub!(:new).with(config.application).and_return(heroku)
31
+ end
32
+
33
+ shared_examples_for "disabled" do
34
+ before { config.enabled = false }
35
+
36
+ it "does nothing" do
37
+ heroku.should_not_receive(:scale_workers)
38
+ perform_action
39
+ end
40
+ end
41
+
42
+ describe "scale up" do
43
+ def perform_action
44
+ manager.scale_up(options)
45
+ end
46
+
47
+ context "when there are no workers running" do
48
+ context "and there is no pending jobs" do
49
+ it "does nothing" do
50
+ heroku.should_not_receive(:scale_workers)
51
+ perform_action
52
+ end
53
+ end
54
+
55
+ DynoScaler.configuration.job_worker_ratio.keys.each do |number_of_workers|
56
+ context "and there is enough pending jobs so as to scale #{number_of_workers} workers" do
57
+ let(:pending_jobs) { config.job_worker_ratio[number_of_workers] }
58
+
59
+ it "scales workers to #{number_of_workers}" do
60
+ heroku.should_receive(:scale_workers).with(number_of_workers)
61
+ perform_action
62
+ end
63
+
64
+ it_should_behave_like "disabled"
65
+ end
66
+ end
67
+ end
68
+
69
+ context "when there is one worker running" do
70
+ let(:workers) { 1 }
71
+
72
+ context "and there is no pending jobs" do
73
+ it "does nothing" do
74
+ heroku.should_not_receive(:scale_workers)
75
+ perform_action
76
+ end
77
+ end
78
+
79
+ context "and there is less pending jobs that would require another worker" do
80
+ let(:pending_jobs) { config.job_worker_ratio[2] - 1 }
81
+
82
+ it "does nothing" do
83
+ heroku.should_not_receive(:scale_workers)
84
+ perform_action
85
+ end
86
+ end
87
+
88
+ context "and there is enough pending jobs as to scale another worker" do
89
+ let(:pending_jobs) { config.job_worker_ratio[2] }
90
+
91
+ it "scales workers" do
92
+ heroku.should_receive(:scale_workers).with(2)
93
+ perform_action
94
+ end
95
+ end
96
+ end
97
+
98
+ context "when there are many workers running" do
99
+ let(:workers) { 4 }
100
+
101
+ context "and there is no pending jobs" do
102
+ it "does nothing" do
103
+ heroku.should_not_receive(:scale_workers)
104
+ perform_action
105
+ end
106
+ end
107
+
108
+ context "and there is less pending jobs that would require another worker" do
109
+ let(:pending_jobs) { config.job_worker_ratio[4] - 1 }
110
+
111
+ it "does nothing" do
112
+ heroku.should_not_receive(:scale_workers)
113
+ perform_action
114
+ end
115
+ end
116
+
117
+ context "and there is enough pending jobs as to scale another worker" do
118
+ let(:pending_jobs) { config.job_worker_ratio[5] }
119
+
120
+ it "scales workers" do
121
+ heroku.should_receive(:scale_workers).with(5)
122
+ perform_action
123
+ end
124
+ end
125
+
126
+ context "and it is the maximum number of workers running" do
127
+ before { config.max_workers = workers }
128
+
129
+ context "and there is enough pending jobs as to scale another worker" do
130
+ let(:pending_jobs) { config.job_worker_ratio[5] }
131
+
132
+ it "does nothing" do
133
+ heroku.should_not_receive(:scale_workers)
134
+ perform_action
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "scale down" do
142
+ def perform_action
143
+ manager.scale_down(options)
144
+ end
145
+
146
+ context "when there are no workers running" do
147
+ it "does nothing" do
148
+ heroku.should_not_receive(:scale_workers)
149
+ perform_action
150
+ end
151
+ end
152
+
153
+ context "when there is one worker running" do
154
+ let(:workers) { 1 }
155
+
156
+ context "and there are no pending jobs" do
157
+ context "and no running jobs" do
158
+ it "scales down" do
159
+ heroku.should_receive(:scale_workers).with(config.min_workers)
160
+ perform_action
161
+ end
162
+
163
+ context "when min_workers is configured with a different value" do
164
+ before { config.min_workers = 1 }
165
+
166
+ it "does nothing" do
167
+ heroku.should_not_receive(:scale_workers)
168
+ perform_action
169
+ end
170
+ end
171
+ end
172
+
173
+ context "but there are many running jobs" do
174
+ let(:running_jobs) { 4 }
175
+
176
+ it "does nothing" do
177
+ heroku.should_not_receive(:scale_workers)
178
+ perform_action
179
+ end
180
+ end
181
+ end
182
+
183
+ context "and there are pending jobs" do
184
+ let(:pending_jobs) { 1 }
185
+
186
+ context "and no running jobs" do
187
+ it "does nothing" do
188
+ heroku.should_not_receive(:scale_workers)
189
+ perform_action
190
+ end
191
+ end
192
+
193
+ context "but there are running jobs" do
194
+ let(:running_jobs) { 1 }
195
+
196
+ it "does nothing" do
197
+ heroku.should_not_receive(:scale_workers)
198
+ perform_action
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ context "when there are many workers running" do
205
+ let(:workers) { 4 }
206
+
207
+ context "and there are no pending jobs" do
208
+ context "and no running jobs" do
209
+ it "scales down" do
210
+ heroku.should_receive(:scale_workers).with(config.min_workers)
211
+ perform_action
212
+ end
213
+
214
+ context "when min_workers is configured with a different value" do
215
+ before { config.min_workers = 2 }
216
+
217
+ it "scales down to the min workers value" do
218
+ heroku.should_receive(:scale_workers).with(config.min_workers)
219
+ perform_action
220
+ end
221
+ end
222
+ end
223
+
224
+ context "but there are many running jobs" do
225
+ let(:running_jobs) { 4 }
226
+
227
+ it "does nothing" do
228
+ heroku.should_not_receive(:scale_workers)
229
+ perform_action
230
+ end
231
+ end
232
+ end
233
+
234
+ context "and there are pending jobs" do
235
+ let(:pending_jobs) { 1 }
236
+
237
+ context "and no running jobs" do
238
+ it "does nothing" do
239
+ heroku.should_not_receive(:scale_workers)
240
+ perform_action
241
+ end
242
+ end
243
+
244
+ context "but there are running jobs" do
245
+ let(:running_jobs) { 1 }
246
+
247
+ it "does nothing" do
248
+ heroku.should_not_receive(:scale_workers)
249
+ perform_action
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ describe "scale with options" do
257
+ let(:action) { :scale_up }
258
+ let(:options) do
259
+ {
260
+ action: action,
261
+ workers: workers,
262
+ pending: pending_jobs,
263
+ working: running_jobs
264
+ }
265
+ end
266
+
267
+ def perform_action
268
+ manager.scale_with(options)
269
+ end
270
+
271
+ context "when action is scale up" do
272
+ it "scales up passing options" do
273
+ manager.should_receive(:scale_up).with(options)
274
+ perform_action
275
+ end
276
+ end
277
+
278
+ context "when action is scale down" do
279
+ let(:action) { :scale_down }
280
+
281
+ it "scales down passing options" do
282
+ manager.should_receive(:scale_down).with(options)
283
+ perform_action
284
+ end
285
+ end
286
+
287
+ context "when no action is provided" do
288
+ let(:options) do
289
+ {
290
+ workers: workers,
291
+ pending: pending_jobs,
292
+ working: running_jobs
293
+ }
294
+ end
295
+
296
+ context "when there are no workers running" do
297
+ context "and there is no pending jobs" do
298
+ it "does nothing" do
299
+ manager.should_not_receive(:scale_up)
300
+ manager.should_not_receive(:scale_down)
301
+ perform_action
302
+ end
303
+ end
304
+
305
+ context "and there is pending jobs" do
306
+ let(:pending_jobs) { 2 }
307
+
308
+ it "scales up" do
309
+ manager.should_receive(:scale_up).with(options)
310
+ perform_action
311
+ end
312
+ end
313
+ end
314
+
315
+ context "when there are workers running" do
316
+ let(:workers) { 4 }
317
+
318
+ context "and there are no pending jobs" do
319
+ context "and no running jobs" do
320
+ it "scales down" do
321
+ manager.should_receive(:scale_down).with(options)
322
+ perform_action
323
+ end
324
+ end
325
+
326
+ context "but there are many running jobs" do
327
+ let(:running_jobs) { 4 }
328
+
329
+ it "does nothing" do
330
+ manager.should_not_receive(:scale_up)
331
+ manager.should_not_receive(:scale_down)
332
+ perform_action
333
+ end
334
+ end
335
+ end
336
+
337
+ context "and there are pending jobs" do
338
+ let(:pending_jobs) { 1 }
339
+
340
+ it "does nothing" do
341
+ manager.should_not_receive(:scale_up)
342
+ manager.should_not_receive(:scale_down)
343
+ perform_action
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,201 @@
1
+ require "spec_helper"
2
+
3
+ require 'resque'
4
+
5
+ class SampleJob
6
+ include DynoScaler::Workers::Resque
7
+
8
+ @queue = :sample
9
+
10
+ def self.perform
11
+ # do something
12
+ end
13
+
14
+ def self.reset!
15
+ @manager = nil
16
+ end
17
+ end
18
+
19
+ describe DynoScaler::Workers::Resque do
20
+ let(:manager) { mock(DynoScaler::Manager, scale_up: false, scale_down: false) }
21
+
22
+ let(:workers) { 0 }
23
+ let(:working) { 0 }
24
+ let(:pending) { 1 }
25
+
26
+ before do
27
+ Resque.stub!(:info).and_return({
28
+ workers: workers,
29
+ working: working,
30
+ pending: pending
31
+ })
32
+
33
+ SampleJob.reset!
34
+ DynoScaler::Manager.stub!(:new).and_return(manager)
35
+ end
36
+
37
+ def work_off(queue)
38
+ job = Resque::Job.reserve(queue)
39
+ job ? job.perform : fail("No jobs for queue '#{queue}'.")
40
+ end
41
+
42
+ describe "when enqueued" do
43
+ it "scales up" do
44
+ manager.should_receive(:scale_up).with(Resque.info)
45
+ Resque.enqueue(SampleJob)
46
+ end
47
+
48
+ context "when there are workers" do
49
+ let(:workers) { 2 }
50
+
51
+ it "passes the number of current workers to the manager" do
52
+ manager.should_receive(:scale_up).with(Resque.info)
53
+ Resque.enqueue(SampleJob)
54
+ end
55
+ end
56
+
57
+ context "when there are pending jobs" do
58
+ let(:pending) { 5 }
59
+
60
+ it "passes the number of pending jobs" do
61
+ manager.should_receive(:scale_up).with(Resque.info)
62
+ Resque.enqueue(SampleJob)
63
+ end
64
+ end
65
+
66
+ context "when scaling up is disabled" do
67
+ before { SampleJob.disable_scaling_up }
68
+ after { SampleJob.enable_scaling_up }
69
+
70
+ it "does not scale up" do
71
+ manager.should_not_receive(:scale_up)
72
+ Resque.enqueue(SampleJob)
73
+ end
74
+ end
75
+
76
+ context "when an error occurs while scaling up" do
77
+ before do
78
+ manager.stub!(:scale_up).and_raise("error")
79
+ end
80
+
81
+ it "does not raises it" do
82
+ capture(:stderr) do
83
+ expect { Resque.enqueue(SampleJob) }.to_not raise_error
84
+ end
85
+ end
86
+
87
+ it "enqueues the job" do
88
+ capture(:stderr) do
89
+ Resque.enqueue(SampleJob)
90
+ work_off(:sample)
91
+ end
92
+ end
93
+
94
+ it "prints a message in $stderr" do
95
+ capture(:stderr) { Resque.enqueue(SampleJob) }.should eq("Could not scale up workers: error\n")
96
+ end
97
+ end
98
+
99
+ context "and async is configured" do
100
+ let(:config) { DynoScaler.configuration }
101
+
102
+ context "with default processor" do
103
+ before { config.async = true }
104
+
105
+ it "calls the given async processor passing the current Resque info and the scale up action" do
106
+ config.async.should_receive(:call).with(Resque.info.merge(action: :scale_up))
107
+ Resque.enqueue(SampleJob)
108
+ end
109
+
110
+ context "and the Girl-Friday job is run" do
111
+ before { GirlFriday::Queue.immediate! }
112
+
113
+ after do
114
+ GirlFriday::Queue.queue!
115
+ DynoScaler.reset!
116
+ end
117
+
118
+ it "runs the scale up with the Resque info and the scale up action" do
119
+ DynoScaler.manager.should_receive(:scale_with).with(Resque.info.merge(action: :scale_up))
120
+ Resque.enqueue(SampleJob)
121
+ end
122
+ end
123
+ end
124
+
125
+ context "with a block" do
126
+ before do
127
+ config.async { |options| :ok }
128
+ end
129
+
130
+ it "calls the given async processor passing the current Resque info and the scale up action" do
131
+ config.async.should_receive(:call).with(Resque.info.merge(action: :scale_up))
132
+ Resque.enqueue(SampleJob)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "after performing" do
139
+ before do
140
+ Resque.enqueue(SampleJob)
141
+ end
142
+
143
+ it "scales down" do
144
+ manager.should_receive(:scale_down).with(Resque.info)
145
+ work_off(:sample)
146
+ end
147
+
148
+ context "when there are workers" do
149
+ let(:workers) { 2 }
150
+
151
+ it "passes the number of current workers to the manager" do
152
+ manager.should_receive(:scale_down).with(Resque.info)
153
+ work_off(:sample)
154
+ end
155
+ end
156
+
157
+ context "when there are pending jobs" do
158
+ let(:pending) { 5 }
159
+
160
+ it "passes the number of pending jobs" do
161
+ manager.should_receive(:scale_down).with(Resque.info)
162
+ work_off(:sample)
163
+ end
164
+ end
165
+
166
+ context "when there are running jobs" do
167
+ let(:working) { 5 }
168
+
169
+ it "passes the number of running jobs minus 1, since we do not count ourselves" do
170
+ manager.should_receive(:scale_down).with(Resque.info.merge(working: Resque.info[:working] - 1))
171
+ work_off(:sample)
172
+ end
173
+ end
174
+
175
+ context "when scaling down is disabled" do
176
+ before { SampleJob.disable_scaling_down }
177
+ after { SampleJob.enable_scaling_down }
178
+
179
+ it "does not scale down" do
180
+ manager.should_not_receive(:scale_down)
181
+ work_off(:sample)
182
+ end
183
+ end
184
+
185
+ context "when an error occurs while scaling down" do
186
+ before do
187
+ manager.stub!(:scale_down).and_raise("error")
188
+ end
189
+
190
+ it "does not raises it" do
191
+ capture(:stderr) do
192
+ expect { work_off(:sample) }.to_not raise_error
193
+ end
194
+ end
195
+
196
+ it "prints a message in $stderr" do
197
+ capture(:stderr) { work_off(:sample) }.should eq("Could not scale down workers: error\n")
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,11 @@
1
+ require "spec_helper"
2
+
3
+ describe DynoScaler do
4
+ it "returns a new configuration" do
5
+ DynoScaler.configuration.should be_a(DynoScaler::Configuration)
6
+ end
7
+
8
+ it "returns a new manager" do
9
+ DynoScaler.manager.should be_a(DynoScaler::Manager)
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ require 'simplecov'
2
+
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ end
6
+
7
+ require 'bundler/setup'
8
+
9
+ ENV['RACK_ENV'] ||= 'test'
10
+
11
+ Bundler.require :default, ENV['RACK_ENV']
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+
18
+ # Run specs in random order to surface order dependencies. If you find an
19
+ # order dependency and want to debug it, you can fix the order by providing
20
+ # the seed, which is printed after each run.
21
+ # --seed 1234
22
+ # config.order = 'random'
23
+
24
+ config.before do
25
+ DynoScaler.configuration.logger = Logger.new(StringIO.new)
26
+ end
27
+
28
+ def capture(stream)
29
+ begin
30
+ stream = stream.to_s
31
+ eval "$#{stream} = StringIO.new"
32
+ yield
33
+ result = eval("$#{stream}").string
34
+ ensure
35
+ eval("$#{stream} = #{stream.upcase}")
36
+ end
37
+
38
+ result
39
+ end
40
+ end