delayed_job 2.1.0.pre → 2.1.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -40,7 +40,7 @@ After delayed_job is installed, you will need to setup the backend.
40
40
 
41
41
  h2. Backends
42
42
 
43
- delayed_job supports multiple backends for storing the job queue. There are currently implementations for Active Record, MongoMapper, and DataMapper.
43
+ delayed_job supports multiple backends for storing the job queue. "See the wiki for other backends":http://wiki.github.com/collectiveidea/delayed_job/backends besides Active Record.
44
44
 
45
45
  h3. Active Record
46
46
 
@@ -51,21 +51,6 @@ $ script/generate delayed_job
51
51
  $ rake db:migrate
52
52
  </pre>
53
53
 
54
- h3. MongoMapper
55
-
56
- <pre>
57
- # config/initializers/delayed_job.rb
58
- Delayed::Worker.backend = :mongo_mapper
59
- </pre>
60
-
61
- h3. DataMapper
62
-
63
- <pre>
64
- # config/initializers/delayed_job.rb
65
- Delayed::Worker.backend = :data_mapper
66
- Delayed::Worker.backend.auto_upgrade!
67
- </pre>
68
-
69
54
  h2. Queuing Jobs
70
55
 
71
56
  Call @.delay.method(params)@ on any object and it will be processed in the background.
@@ -25,11 +25,18 @@ module Delayed
25
25
 
26
26
  before_save :set_default_run_at
27
27
 
28
- named_scope :ready_to_run, lambda {|worker_name, max_run_time|
29
- {:conditions => ['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name]}
30
- }
31
- named_scope :by_priority, :order => 'priority ASC, run_at ASC'
32
-
28
+ if ::ActiveRecord::VERSION::MAJOR >= 3
29
+ scope :ready_to_run, lambda {|worker_name, max_run_time|
30
+ where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
31
+ }
32
+ scope :by_priority, order('priority ASC, run_at ASC')
33
+ else
34
+ named_scope :ready_to_run, lambda {|worker_name, max_run_time|
35
+ {:conditions => ['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name]}
36
+ }
37
+ named_scope :by_priority, :order => 'priority ASC, run_at ASC'
38
+ end
39
+
33
40
  def self.after_fork
34
41
  ::ActiveRecord::Base.connection.reconnect!
35
42
  end
@@ -16,7 +16,7 @@ module Delayed
16
16
  raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
17
17
  end
18
18
 
19
- priority = args.first || 0
19
+ priority = args.first || Delayed::Worker.default_priority
20
20
  run_at = args[1]
21
21
  self.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
22
22
  end
@@ -62,7 +62,16 @@ module Delayed
62
62
 
63
63
  # Moved into its own method so that new_relic can trace it.
64
64
  def invoke_job
65
- payload_object.perform
65
+ payload_object.before(self) if payload_object.respond_to?(:before)
66
+ begin
67
+ payload_object.perform
68
+ payload_object.success(self) if payload_object.respond_to?(:success)
69
+ rescue Exception => e
70
+ payload_object.failure(self, e) if payload_object.respond_to?(:failure)
71
+ raise e
72
+ ensure
73
+ payload_object.after(self) if payload_object.respond_to?(:after)
74
+ end
66
75
  end
67
76
 
68
77
  # Unlock this job (note: not saved to DB)
@@ -79,4 +88,4 @@ module Delayed
79
88
 
80
89
  end
81
90
  end
82
- end
91
+ end
@@ -0,0 +1,472 @@
1
+ require File.expand_path('../../../../spec/sample_jobs', __FILE__)
2
+
3
+ shared_examples_for 'a delayed_job backend' do
4
+ def create_job(opts = {})
5
+ described_class.create(opts.merge(:payload_object => SimpleJob.new))
6
+ end
7
+
8
+ before do
9
+ Delayed::Worker.max_priority = nil
10
+ Delayed::Worker.min_priority = nil
11
+ Delayed::Worker.default_priority = 99
12
+ SimpleJob.runs = 0
13
+ described_class.delete_all
14
+ end
15
+
16
+ it "should set run_at automatically if not set" do
17
+ described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
18
+ end
19
+
20
+ it "should not set run_at automatically if already set" do
21
+ later = described_class.db_time_now + 5.minutes
22
+ described_class.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
23
+ end
24
+
25
+ describe "enqueue" do
26
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
27
+ lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should increase count after enqueuing items" do
31
+ described_class.enqueue SimpleJob.new
32
+ described_class.count.should == 1
33
+ end
34
+
35
+ it "should be able to set priority" do
36
+ @job = described_class.enqueue SimpleJob.new, 5
37
+ @job.priority.should == 5
38
+ end
39
+
40
+ it "should use default priority when it is not set" do
41
+ @job = described_class.enqueue SimpleJob.new
42
+ @job.priority.should == 99
43
+ end
44
+
45
+ it "should be able to set run_at" do
46
+ later = described_class.db_time_now + 5.minutes
47
+ @job = described_class.enqueue SimpleJob.new, 5, later
48
+ @job.run_at.should be_close(later, 1)
49
+ end
50
+
51
+ it "should work with jobs in modules" do
52
+ M::ModuleJob.runs = 0
53
+ job = described_class.enqueue M::ModuleJob.new
54
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
55
+ end
56
+ end
57
+
58
+ describe "callbacks" do
59
+ before(:each) do
60
+ SuccessfulCallbackJob.messages = []
61
+ FailureCallbackJob.messages = []
62
+ end
63
+
64
+ it "should call before and after callbacks" do
65
+ job = described_class.enqueue(SuccessfulCallbackJob.new)
66
+ job.invoke_job
67
+ SuccessfulCallbackJob.messages.should == ["before perform", "perform", "success!", "after perform"]
68
+ end
69
+
70
+ it "should call the after callback with an error" do
71
+ job = described_class.enqueue(FailureCallbackJob.new)
72
+ lambda {job.invoke_job}.should raise_error
73
+ FailureCallbackJob.messages.should == ["before perform", "error: RuntimeError", "after perform"]
74
+ end
75
+
76
+ end
77
+
78
+ describe "payload_object" do
79
+ it "should raise a DeserializationError when the job class is totally unknown" do
80
+ job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
81
+ lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
82
+ end
83
+
84
+ it "should raise a DeserializationError when the job struct is totally unknown" do
85
+ job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
86
+ lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
87
+ end
88
+ end
89
+
90
+ describe "find_available" do
91
+ it "should not find failed jobs" do
92
+ @job = create_job :attempts => 50, :failed_at => described_class.db_time_now
93
+ described_class.find_available('worker', 5, 1.second).should_not include(@job)
94
+ end
95
+
96
+ it "should not find jobs scheduled for the future" do
97
+ @job = create_job :run_at => (described_class.db_time_now + 1.minute)
98
+ described_class.find_available('worker', 5, 4.hours).should_not include(@job)
99
+ end
100
+
101
+ it "should not find jobs locked by another worker" do
102
+ @job = create_job(:locked_by => 'other_worker', :locked_at => described_class.db_time_now - 1.minute)
103
+ described_class.find_available('worker', 5, 4.hours).should_not include(@job)
104
+ end
105
+
106
+ it "should find open jobs" do
107
+ @job = create_job
108
+ described_class.find_available('worker', 5, 4.hours).should include(@job)
109
+ end
110
+
111
+ it "should find expired jobs" do
112
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now - 2.minutes)
113
+ described_class.find_available('worker', 5, 1.minute).should include(@job)
114
+ end
115
+
116
+ it "should find own jobs" do
117
+ @job = create_job(:locked_by => 'worker', :locked_at => (described_class.db_time_now - 1.minutes))
118
+ described_class.find_available('worker', 5, 4.hours).should include(@job)
119
+ end
120
+
121
+ it "should find only the right amount of jobs" do
122
+ 10.times { create_job }
123
+ described_class.find_available('worker', 7, 4.hours).should have(7).jobs
124
+ end
125
+ end
126
+
127
+ context "when another worker is already performing an task, it" do
128
+ before :each do
129
+ @job = described_class.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => described_class.db_time_now - 5.minutes
130
+ end
131
+
132
+ it "should not allow a second worker to get exclusive access" do
133
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
134
+ end
135
+
136
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
137
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
138
+ end
139
+
140
+ it "should be able to get access to the task if it was started more then max_age ago" do
141
+ @job.locked_at = described_class.db_time_now - 5.hours
142
+ @job.save
143
+
144
+ @job.lock_exclusively! 4.hours, 'worker2'
145
+ @job.reload
146
+ @job.locked_by.should == 'worker2'
147
+ @job.locked_at.should > (described_class.db_time_now - 1.minute)
148
+ end
149
+
150
+ it "should not be found by another worker" do
151
+ described_class.find_available('worker2', 1, 6.minutes).length.should == 0
152
+ end
153
+
154
+ it "should be found by another worker if the time has expired" do
155
+ described_class.find_available('worker2', 1, 4.minutes).length.should == 1
156
+ end
157
+
158
+ it "should be able to get exclusive access again when the worker name is the same" do
159
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
160
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
161
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
162
+ end
163
+ end
164
+
165
+ context "when another worker has worked on a task since the job was found to be available, it" do
166
+
167
+ before :each do
168
+ @job = described_class.create :payload_object => SimpleJob.new
169
+ @job_copy_for_worker_2 = described_class.find(@job.id)
170
+ end
171
+
172
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
173
+ @job.destroy
174
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
175
+ end
176
+
177
+ it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
178
+ @job.update_attributes(:attempts => 1, :run_at => described_class.db_time_now + 1.day)
179
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
180
+ end
181
+ end
182
+
183
+ context "#name" do
184
+ it "should be the class name of the job that was enqueued" do
185
+ described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
186
+ end
187
+
188
+ it "should be the method that will be called if its a performable method object" do
189
+ job = described_class.new(:payload_object => NamedJob.new)
190
+ job.name.should == 'named_job'
191
+ end
192
+
193
+ it "should be the instance method that will be called if its a performable method object" do
194
+ @job = Story.create(:text => "...").delay.save
195
+ @job.name.should == 'Story#save'
196
+ end
197
+ end
198
+
199
+ context "worker prioritization" do
200
+ before(:each) do
201
+ Delayed::Worker.max_priority = nil
202
+ Delayed::Worker.min_priority = nil
203
+ end
204
+
205
+ it "should fetch jobs ordered by priority" do
206
+ 10.times { described_class.enqueue SimpleJob.new, rand(10) }
207
+ jobs = described_class.find_available('worker', 10)
208
+ jobs.size.should == 10
209
+ jobs.each_cons(2) do |a, b|
210
+ a.priority.should <= b.priority
211
+ end
212
+ end
213
+
214
+ it "should only find jobs greater than or equal to min priority" do
215
+ min = 5
216
+ Delayed::Worker.min_priority = min
217
+ 10.times {|i| described_class.enqueue SimpleJob.new, i }
218
+ jobs = described_class.find_available('worker', 10)
219
+ jobs.each {|job| job.priority.should >= min}
220
+ end
221
+
222
+ it "should only find jobs less than or equal to max priority" do
223
+ max = 5
224
+ Delayed::Worker.max_priority = max
225
+ 10.times {|i| described_class.enqueue SimpleJob.new, i }
226
+ jobs = described_class.find_available('worker', 10)
227
+ jobs.each {|job| job.priority.should <= max}
228
+ end
229
+ end
230
+
231
+ context "clear_locks!" do
232
+ before do
233
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
234
+ end
235
+
236
+ it "should clear locks for the given worker" do
237
+ described_class.clear_locks!('worker')
238
+ described_class.find_available('worker2', 5, 1.minute).should include(@job)
239
+ end
240
+
241
+ it "should not clear locks for other workers" do
242
+ described_class.clear_locks!('worker1')
243
+ described_class.find_available('worker1', 5, 1.minute).should_not include(@job)
244
+ end
245
+ end
246
+
247
+ context "unlock" do
248
+ before do
249
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
250
+ end
251
+
252
+ it "should clear locks" do
253
+ @job.unlock
254
+ @job.locked_by.should be_nil
255
+ @job.locked_at.should be_nil
256
+ end
257
+ end
258
+
259
+ context "large handler" do
260
+ before do
261
+ text = "Lorem ipsum dolor sit amet. " * 1000
262
+ @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
263
+ end
264
+
265
+ it "should have an id" do
266
+ @job.id.should_not be_nil
267
+ end
268
+ end
269
+
270
+ describe "yaml serialization" do
271
+ it "should reload changed attributes" do
272
+ job = described_class.enqueue SimpleJob.new
273
+ yaml = job.to_yaml
274
+ job.priority = 99
275
+ job.save
276
+ YAML.load(yaml).priority.should == 99
277
+ end
278
+
279
+ it "should ignore destroyed records" do
280
+ job = described_class.enqueue SimpleJob.new
281
+ yaml = job.to_yaml
282
+ job.destroy
283
+ lambda { YAML.load(yaml).should be_nil }.should_not raise_error
284
+ end
285
+ end
286
+
287
+ describe "worker integration" do
288
+ before do
289
+ Delayed::Job.delete_all
290
+
291
+ @worker = Delayed::Worker.new(:max_priority => nil, :min_priority => nil, :quiet => true)
292
+
293
+ SimpleJob.runs = 0
294
+ end
295
+
296
+ describe "running a job" do
297
+ it "should fail after Worker.max_run_time" do
298
+ begin
299
+ old_max_run_time = Delayed::Worker.max_run_time
300
+ Delayed::Worker.max_run_time = 1.second
301
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
302
+ @worker.run(@job)
303
+ @job.reload.last_error.should =~ /expired/
304
+ @job.attempts.should == 1
305
+ ensure
306
+ Delayed::Worker.max_run_time = old_max_run_time
307
+ end
308
+ end
309
+ end
310
+
311
+ context "worker prioritization" do
312
+ before(:each) do
313
+ @worker = Delayed::Worker.new(:max_priority => 5, :min_priority => -5, :quiet => true)
314
+ end
315
+
316
+ it "should only work_off jobs that are >= min_priority" do
317
+ create_job(:priority => -10)
318
+ create_job(:priority => 0)
319
+ @worker.work_off
320
+
321
+ SimpleJob.runs.should == 1
322
+ end
323
+
324
+ it "should only work_off jobs that are <= max_priority" do
325
+ create_job(:priority => 10)
326
+ create_job(:priority => 0)
327
+
328
+ @worker.work_off
329
+
330
+ SimpleJob.runs.should == 1
331
+ end
332
+ end
333
+
334
+ context "while running with locked and expired jobs" do
335
+ before(:each) do
336
+ @worker.name = 'worker1'
337
+ end
338
+
339
+ it "should not run jobs locked by another worker" do
340
+ create_job(:locked_by => 'other_worker', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
341
+ lambda { @worker.work_off }.should_not change { SimpleJob.runs }
342
+ end
343
+
344
+ it "should run open jobs" do
345
+ create_job
346
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
347
+ end
348
+
349
+ it "should run expired jobs" do
350
+ expired_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Worker.max_run_time)
351
+ create_job(:locked_by => 'other_worker', :locked_at => expired_time)
352
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
353
+ end
354
+
355
+ it "should run own jobs" do
356
+ create_job(:locked_by => @worker.name, :locked_at => (Delayed::Job.db_time_now - 1.minutes))
357
+ lambda { @worker.work_off }.should change { SimpleJob.runs }.from(0).to(1)
358
+ end
359
+ end
360
+
361
+ describe "failed jobs" do
362
+ before do
363
+ # reset defaults
364
+ Delayed::Worker.destroy_failed_jobs = true
365
+ Delayed::Worker.max_attempts = 25
366
+
367
+ @job = Delayed::Job.enqueue ErrorJob.new
368
+ end
369
+
370
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
371
+ Delayed::Worker.destroy_failed_jobs = false
372
+ Delayed::Worker.max_attempts = 1
373
+ @worker.run(@job)
374
+ @job.reload
375
+ @job.last_error.should =~ /did not work/
376
+ @job.attempts.should == 1
377
+ @job.failed_at.should_not be_nil
378
+ end
379
+
380
+ it "should re-schedule jobs after failing" do
381
+ @worker.run(@job)
382
+ @job.reload
383
+ @job.last_error.should =~ /did not work/
384
+ @job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
385
+ @job.attempts.should == 1
386
+ @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
387
+ @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
388
+ end
389
+ end
390
+
391
+ context "reschedule" do
392
+ before do
393
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
394
+ end
395
+
396
+ share_examples_for "any failure more than Worker.max_attempts times" do
397
+ context "when the job's payload has an #on_permanent_failure hook" do
398
+ before do
399
+ @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
400
+ @job.payload_object.should respond_to :on_permanent_failure
401
+ end
402
+
403
+ it "should run that hook" do
404
+ @job.payload_object.should_receive :on_permanent_failure
405
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
406
+ end
407
+ end
408
+
409
+ context "when the job's payload has no #on_permanent_failure hook" do
410
+ # It's a little tricky to test this in a straightforward way,
411
+ # because putting a should_not_receive expectation on
412
+ # @job.payload_object.on_permanent_failure makes that object
413
+ # incorrectly return true to
414
+ # payload_object.respond_to? :on_permanent_failure, which is what
415
+ # reschedule uses to decide whether to call on_permanent_failure.
416
+ # So instead, we just make sure that the payload_object as it
417
+ # already stands doesn't respond_to? on_permanent_failure, then
418
+ # shove it through the iterated reschedule loop and make sure we
419
+ # don't get a NoMethodError (caused by calling that nonexistent
420
+ # on_permanent_failure method).
421
+
422
+ before do
423
+ @job.payload_object.should_not respond_to(:on_permanent_failure)
424
+ end
425
+
426
+ it "should not try to run that hook" do
427
+ lambda do
428
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
429
+ end.should_not raise_exception(NoMethodError)
430
+ end
431
+ end
432
+ end
433
+
434
+ context "and we want to destroy jobs" do
435
+ before do
436
+ Delayed::Worker.destroy_failed_jobs = true
437
+ end
438
+
439
+ it_should_behave_like "any failure more than Worker.max_attempts times"
440
+
441
+ it "should be destroyed if it failed more than Worker.max_attempts times" do
442
+ @job.should_receive(:destroy)
443
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
444
+ end
445
+
446
+ it "should not be destroyed if failed fewer than Worker.max_attempts times" do
447
+ @job.should_not_receive(:destroy)
448
+ (Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
449
+ end
450
+ end
451
+
452
+ context "and we don't want to destroy jobs" do
453
+ before do
454
+ Delayed::Worker.destroy_failed_jobs = false
455
+ end
456
+
457
+ it_should_behave_like "any failure more than Worker.max_attempts times"
458
+
459
+ it "should be failed if it failed more than Worker.max_attempts times" do
460
+ @job.reload.failed_at.should == nil
461
+ Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
462
+ @job.reload.failed_at.should_not == nil
463
+ end
464
+
465
+ it "should not be failed if it failed fewer than Worker.max_attempts times" do
466
+ (Delayed::Worker.max_attempts - 1).times { @worker.reschedule(@job) }
467
+ @job.reload.failed_at.should == nil
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end