delayed_job 2.1.0.pre → 2.1.0.pre2

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