samoli-hirefire 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Load the "HireFire modified"
5
+ # Resque::Job and Resque::Worker classes
6
+ require File.dirname(__FILE__) + '/resque/job'
7
+ require File.dirname(__FILE__) + '/resque/worker'
8
+
9
+ ##
10
+ # HireFire
11
+ # This is a HireFire modified version of
12
+ # the official Resque module
13
+ module ::Resque
14
+ def self.enqueue(klass, *args)
15
+ Job.create(queue_from_class(klass), klass, *args)
16
+
17
+ ##
18
+ # HireFire Hook
19
+ # After a new job gets queued, we command the current environment
20
+ # to calculate the amount of workers we need to process the jobs
21
+ # that are currently queued, and hire them accordingly.
22
+ if ::Resque.info[:working].to_i == 0 \
23
+ or ::Resque.info[:jobs] == 1
24
+ ::Resque::Job.environment.hire
25
+ end
26
+
27
+ Plugin.after_enqueue_hooks(klass).each do |hook|
28
+ klass.send(hook, *args)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # HireFire
5
+ # This is a HireFire modified version of
6
+ # the official Resque::Job class
7
+ module ::Resque
8
+ class Job
9
+ def perform
10
+ job = payload_class
11
+ job_args = args || []
12
+ job_was_performed = false
13
+
14
+ before_hooks = Plugin.before_hooks(job)
15
+ around_hooks = Plugin.around_hooks(job)
16
+ after_hooks = Plugin.after_hooks(job)
17
+ failure_hooks = Plugin.failure_hooks(job)
18
+
19
+ begin
20
+ begin
21
+ before_hooks.each do |hook|
22
+ job.send(hook, *job_args)
23
+ end
24
+ rescue DontPerform
25
+ return false
26
+ end
27
+
28
+ if around_hooks.empty?
29
+ job.perform(*job_args)
30
+ job_was_performed = true
31
+ else
32
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
33
+ if last_hook
34
+ lambda do
35
+ job.send(hook, *job_args) { last_hook.call }
36
+ end
37
+ else
38
+ lambda do
39
+ job.send(hook, *job_args) do
40
+ result = job.perform(*job_args)
41
+ job_was_performed = true
42
+ result
43
+ end
44
+ end
45
+ end
46
+ end
47
+ stack.call
48
+ end
49
+
50
+ ##
51
+ # HireFire Hook
52
+ # After a job finishes processing, we invoke the #fire
53
+ # method on the environment object which will check to see whether
54
+ # we can fire all the hired workers
55
+ ::Resque::Job.environment.fire
56
+
57
+ after_hooks.each do |hook|
58
+ job.send(hook, *job_args)
59
+ end
60
+
61
+ return job_was_performed
62
+
63
+ rescue Object => e
64
+ failure_hooks.each { |hook| job.send(hook, e, *job_args) }
65
+ raise e
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # Load in the official Resque rake tasks
5
+ require 'resque/tasks'
6
+
7
+ ##
8
+ # Overwrite the resque:setup rake task to first load
9
+ # in the application environment before proceeding
10
+ #
11
+ # ENV['QUEUE'] will default to '*' unless it's defined
12
+ # as an environment variable on Heroku or the Local machine
13
+ task 'resque:setup' => :environment do
14
+ ENV['QUEUE'] ||= '*'
15
+ end
16
+
17
+ ##
18
+ # This is an alias to the "resque:work" task since Heroku doesn't respond
19
+ # to "resque:work", we need to add this alias so Resque can be initialized by Heroku
20
+ desc 'Alias of "resque:work" - This is required for running on Heroku.'
21
+ task 'jobs:work' => 'resque:work'
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ ##
4
+ # HireFire
5
+ # This is a HireFire modified version of
6
+ # the official Resque::Worker class
7
+ module ::Resque
8
+ class Worker
9
+ def work(interval = 5.0, &block)
10
+ interval = Float(interval)
11
+ $0 = "resque: Starting"
12
+ startup
13
+
14
+ loop do
15
+ break if shutdown?
16
+ ::Resque::Job.environment.hire
17
+
18
+ if not @paused and job = reserve
19
+ log "got: #{job.inspect}"
20
+ run_hook :before_fork, job
21
+ working_on job
22
+
23
+ if @child = fork
24
+ rand # Reseeding
25
+ procline "Forked #{@child} at #{Time.now.to_i}"
26
+ Process.wait
27
+ else
28
+ procline "Processing #{job.queue} since #{Time.now.to_i}"
29
+ perform(job, &block)
30
+ exit! unless @cant_fork
31
+ end
32
+
33
+ done_working
34
+ @child = nil
35
+ else
36
+
37
+ ##
38
+ # HireFire Hook
39
+ # After the last job in the queue finishes processing, Resque::Job.jobs will return 0.
40
+ # This means that there aren't any more jobs to process for any of the workers.
41
+ # If this is the case it'll command the current environment to fire all the hired workers
42
+ # and then immediately break out of this infinite loop.
43
+ if ::Resque::Job.jobs == 0
44
+ ::Resque::Job.environment.fire
45
+ break
46
+ else
47
+ sleep(interval)
48
+ end
49
+
50
+ end
51
+ end
52
+
53
+ ensure
54
+ unregister_worker
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+
5
+ describe HireFire::Configuration do
6
+
7
+ it 'should have defaults' do
8
+ configuration = HireFire.configuration
9
+
10
+ configuration.environment.should == nil
11
+ configuration.max_workers.should == 1
12
+ configuration.min_workers.should == 0
13
+ configuration.job_worker_ratio.should == [
14
+ { :jobs => 1, :workers => 1 },
15
+ { :jobs => 25, :workers => 2 },
16
+ { :jobs => 50, :workers => 3 },
17
+ { :jobs => 75, :workers => 4 },
18
+ { :jobs => 100, :workers => 5 }
19
+ ]
20
+ end
21
+
22
+ it 'should be configurable' do
23
+ HireFire.configure do |config|
24
+ config.environment = :noop
25
+ config.max_workers = 10
26
+ config.min_workers = 0
27
+ config.job_worker_ratio = [
28
+ { :jobs => 1, :workers => 1 },
29
+ { :jobs => 15, :workers => 2 },
30
+ { :jobs => 35, :workers => 3 },
31
+ { :jobs => 60, :workers => 4 },
32
+ { :jobs => 80, :workers => 5 }
33
+ ]
34
+ end
35
+
36
+ configuration = HireFire.configuration
37
+
38
+ configuration.environment.should == :noop
39
+ configuration.max_workers.should == 10
40
+ configuration.min_workers.should == 0
41
+ configuration.job_worker_ratio.should == [
42
+ { :jobs => 1, :workers => 1 },
43
+ { :jobs => 15, :workers => 2 },
44
+ { :jobs => 35, :workers => 3 },
45
+ { :jobs => 60, :workers => 4 },
46
+ { :jobs => 80, :workers => 5 }
47
+ ]
48
+ end
49
+
50
+ it 'should allow functional syntax' do
51
+ ratio = [
52
+ { :when => lambda {|jobs| jobs < 15 }, :workers => 1 },
53
+ { :when => lambda {|jobs| jobs < 35 }, :workers => 2 },
54
+ { :when => lambda {|jobs| jobs < 60 }, :workers => 3 },
55
+ { :when => lambda {|jobs| jobs < 80 }, :workers => 4 }
56
+ ]
57
+
58
+ HireFire.configure do |config|
59
+ config.environment = :noop
60
+ config.max_workers = 10
61
+ config.job_worker_ratio = ratio
62
+ end
63
+
64
+ configuration = HireFire.configuration
65
+
66
+ configuration.environment.should == :noop
67
+ configuration.max_workers.should == 10
68
+ configuration.job_worker_ratio.should == ratio
69
+ end
70
+ end
@@ -0,0 +1,369 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+
5
+ module HireFire
6
+ module Backend
7
+ ##
8
+ # Stub out backend interface inclusion
9
+ # since it's irrelevant for these tests
10
+ def self.included(base)
11
+ base.send(:include, Environment::Stub)
12
+ end
13
+ end
14
+
15
+ module Environment
16
+ module Stub
17
+ ##
18
+ # Stubbed out since this normally comes from
19
+ # a sub class like HireFire::Environment::Heroku or
20
+ # HireFire::Environment::Local
21
+ def workers(amount = nil)
22
+ if amount.nil?
23
+ @_workers ||= 0
24
+ return @_workers
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Allows the specs to stub the workers count
30
+ # and return the desired amount
31
+ def workers=(amount)
32
+ @_workers = amount
33
+ self.stubs(:workers).with.returns(amount)
34
+ end
35
+
36
+ ##
37
+ # Returns the amount of jobs
38
+ # Defaults to: 0
39
+ def jobs
40
+ @_jobs ||= 0
41
+ end
42
+
43
+ ##
44
+ # Allows the specs to stub the queued job count
45
+ # and return the desired amount
46
+ def jobs=(amount)
47
+ @_jobs = amount
48
+ self.stubs(:jobs).returns(amount)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ describe HireFire::Environment::Base do
55
+
56
+ let(:base) { HireFire::Environment::Base.new }
57
+
58
+ describe 'testing the test setup' do
59
+ it 'should default the queued job count to 0 for these specs' do
60
+ base.jobs.should == 0
61
+ end
62
+
63
+ it 'should set the queued job count to 10' do
64
+ base.jobs = 10
65
+ base.jobs.should == 10
66
+ end
67
+
68
+ it 'should have zero workers by default' do
69
+ base.workers.should == 0
70
+ end
71
+
72
+ it 'should set the amount of workers to 10' do
73
+ base.workers = 10
74
+ base.workers.should == 10
75
+ end
76
+ end
77
+
78
+ describe '#fire' do
79
+ it 'should not fire any workers when there are still jobs in the queue' do
80
+ base.jobs = 1
81
+ base.workers = 1
82
+
83
+ base.expects(:workers).with(0).never
84
+ base.fire
85
+ end
86
+
87
+ it 'should not fire any workers if there arent any workers' do
88
+ base.jobs = 1
89
+ base.workers = 0
90
+
91
+ base.expects(:workers).with(0).never
92
+ base.fire
93
+ end
94
+
95
+ it 'should fire all workers when there arent any jobs' do
96
+ base.jobs = 0
97
+ base.workers = 1
98
+
99
+ HireFire::Logger.expects(:message).with('All queued jobs have been processed. Firing all workers.')
100
+ base.expects(:workers).with(0).once
101
+ base.fire
102
+ end
103
+
104
+ it 'should set the workers to minimum workers when there arent any jobs' do
105
+ base.jobs = 0
106
+ base.workers = 10
107
+ base.stubs(:min_workers).returns(2)
108
+
109
+ HireFire::Logger.expects(:message).with('All queued jobs have been processed. Setting workers to 2.')
110
+ base.expects(:workers).with(2).once
111
+ base.fire
112
+ end
113
+ end
114
+
115
+ describe '#hire' do
116
+ describe 'the standard notation' do
117
+ before do
118
+ base.stubs(:max_workers).returns(5)
119
+ base.stubs(:ratio).returns([
120
+ { :jobs => 1, :workers => 1 },
121
+ { :jobs => 15, :workers => 2 },
122
+ { :jobs => 30, :workers => 3 },
123
+ { :jobs => 60, :workers => 4 },
124
+ { :jobs => 90, :workers => 5 }
125
+ ])
126
+ end
127
+
128
+ it 'should request 1 worker' do
129
+ base.jobs = 1
130
+ base.workers = 0
131
+
132
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 1 in total.')
133
+ base.expects(:workers).with(1).once
134
+ base.hire
135
+ end
136
+
137
+ it 'should not request 1 worker, since there already is one worker running' do
138
+ base.jobs = 5
139
+ base.workers = 1
140
+
141
+ base.expects(:workers).with(1).never
142
+ base.hire
143
+ end
144
+
145
+ it 'should request 2 workers' do
146
+ base.jobs = 15
147
+ base.workers = 0
148
+
149
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.')
150
+ base.expects(:workers).with(2).once
151
+ base.hire
152
+ end
153
+
154
+ it 'should request 2 workers' do
155
+ base.jobs = 20
156
+ base.workers = 1
157
+
158
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.')
159
+ base.expects(:workers).with(2).once
160
+ base.hire
161
+ end
162
+
163
+ it 'should not request 2 workers since we already have 2' do
164
+ base.jobs = 25
165
+ base.workers = 2
166
+
167
+ base.expects(:workers).with(2).never
168
+ base.hire
169
+ end
170
+
171
+ it 'should NEVER lower the worker amount from the #hire method' do
172
+ base.jobs = 25 # simulate that 5 jobs are already processed (30 - 5)
173
+ base.workers = 3 # and 3 workers are hired
174
+
175
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.').never
176
+ base.expects(:workers).with(2).never
177
+ base.hire
178
+ end
179
+
180
+ it 'should NEVER hire more workers than the #max_workers' do
181
+ base.jobs = 100
182
+ base.workers = 0
183
+
184
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
185
+
186
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').once
187
+ base.expects(:workers).with(3).once
188
+ base.hire
189
+ end
190
+
191
+ it 'should not hire 5 workers even if defined in the job/ratio, when the limit is 3, it should hire 3 max' do
192
+ base.jobs = 100
193
+ base.workers = 0
194
+
195
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
196
+ base.stubs(:ratio).returns([
197
+ { :jobs => 5, :workers => 5 }
198
+ ])
199
+
200
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').once
201
+ base.expects(:workers).with(3).once
202
+ base.hire
203
+ end
204
+
205
+ it 'should not hire (or invoke) any more workers since the max amount allowed is already running' do
206
+ base.jobs = 100
207
+ base.workers = 3
208
+
209
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
210
+ base.stubs(:ratio).returns([
211
+ { :jobs => 5, :workers => 5 }
212
+ ])
213
+
214
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').never
215
+ base.expects(:workers).with(3).never
216
+ base.hire
217
+ end
218
+
219
+ it 'the max_workers option can only "limit" the amount of max_workers when used in the "Standard Notation"' do
220
+ base.jobs = 100
221
+ base.workers = 0
222
+
223
+ base.stubs(:max_workers).returns(10) # set the max_workers = 10
224
+ base.stubs(:ratio).returns([
225
+ { :jobs => 5, :workers => 5 }
226
+ ])
227
+
228
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 5 in total.').once
229
+ base.expects(:workers).with(5).once
230
+ base.hire
231
+ end
232
+
233
+ it 'should NEVER do API requests to Heroku if the max_workers are already running' do
234
+ base.jobs = 100
235
+ base.workers = 5
236
+
237
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 5 in total.').never
238
+ base.expects(:workers).with(5).never
239
+ base.hire
240
+ end
241
+ end
242
+
243
+ describe 'the Lambda (functional) notation' do
244
+ before do
245
+ base.stubs(:max_workers).returns(5)
246
+ base.stubs(:ratio).returns([
247
+ { :when => lambda {|jobs| jobs < 15 }, :workers => 1 },
248
+ { :when => lambda {|jobs| jobs < 30 }, :workers => 2 },
249
+ { :when => lambda {|jobs| jobs < 60 }, :workers => 3 },
250
+ { :when => lambda {|jobs| jobs < 90 }, :workers => 4 }
251
+ ])
252
+ end
253
+
254
+ it 'should request 1 worker' do
255
+ base.jobs = 1
256
+ base.workers = 0
257
+
258
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 1 in total.')
259
+ base.expects(:workers).with(1).once
260
+ base.hire
261
+ end
262
+
263
+ it 'should not request 1 worker, since there already is one worker running' do
264
+ base.jobs = 5
265
+ base.workers = 1
266
+
267
+ base.expects(:workers).with(1).never
268
+ base.hire
269
+ end
270
+
271
+ it 'should request 2 workers' do
272
+ base.jobs = 15
273
+ base.workers = 0
274
+
275
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.')
276
+ base.expects(:workers).with(2).once
277
+ base.hire
278
+ end
279
+
280
+ it 'should request 2 workers' do
281
+ base.jobs = 20
282
+ base.workers = 1
283
+
284
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.')
285
+ base.expects(:workers).with(2).once
286
+ base.hire
287
+ end
288
+
289
+ it 'should not request 2 workers since we already have 2' do
290
+ base.jobs = 25
291
+ base.workers = 2
292
+
293
+ base.expects(:workers).with(2).never
294
+ base.hire
295
+ end
296
+
297
+ it 'should NEVER lower the worker amount from the #hire method' do
298
+ base.jobs = 25 # simulate that 5 jobs are already processed (30 - 5)
299
+ base.workers = 3 # and 3 workers are hired
300
+
301
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 2 in total.').never
302
+ base.expects(:workers).with(2).never
303
+ base.hire
304
+ end
305
+
306
+ it 'should NEVER hire more workers than the #max_workers' do
307
+ base.jobs = 100
308
+ base.workers = 0
309
+
310
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
311
+
312
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').once
313
+ base.expects(:workers).with(3).once
314
+ base.hire
315
+ end
316
+
317
+ it 'should not hire 5 workers even if defined in the job/ratio, when the limit is 3, it should hire 3 max' do
318
+ base.jobs = 100
319
+ base.workers = 0
320
+
321
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
322
+ base.stubs(:ratio).returns([
323
+ { :when => lambda { |jobs| jobs < 5 }, :workers => 5 }
324
+ ])
325
+
326
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').once
327
+ base.expects(:workers).with(3).once
328
+ base.hire
329
+ end
330
+
331
+ it 'should not hire (or invoke) any more workers since the max amount allowed is already running' do
332
+ base.jobs = 100
333
+ base.workers = 3
334
+
335
+ base.stubs(:max_workers).returns(3) # set the max_workers = 3
336
+ base.stubs(:ratio).returns([
337
+ { :when => lambda { |jobs| jobs < 5 }, :workers => 5 }
338
+ ])
339
+
340
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 3 in total.').never
341
+ base.expects(:workers).with(3).never
342
+ base.hire
343
+ end
344
+
345
+ it 'the max_workers option can only "limit" the amount of max_workers when used in the "Standard Notation"' do
346
+ base.jobs = 100
347
+ base.workers = 0
348
+
349
+ base.stubs(:max_workers).returns(10) # set the max_workers = 10
350
+ base.stubs(:ratio).returns([
351
+ { :when => lambda { |jobs| jobs < 5 }, :workers => 5 }
352
+ ])
353
+
354
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 10 in total.').once
355
+ base.expects(:workers).with(10).once
356
+ base.hire
357
+ end
358
+
359
+ it 'should NEVER do API requests to Heroku if the max_workers are already running' do
360
+ base.jobs = 100
361
+ base.workers = 5
362
+
363
+ HireFire::Logger.expects(:message).with('Hiring more workers so we have 5 in total.').never
364
+ base.expects(:workers).with(5).never
365
+ base.hire
366
+ end
367
+ end
368
+ end
369
+ end