dyno_scaler 0.1.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.
@@ -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