drewda_delayed_job 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +314 -0
  3. data/contrib/delayed_job.monitrc +14 -0
  4. data/contrib/delayed_job_multiple.monitrc +23 -0
  5. data/lib/delayed/backend/base.rb +184 -0
  6. data/lib/delayed/backend/shared_spec.rb +595 -0
  7. data/lib/delayed/command.rb +108 -0
  8. data/lib/delayed/deserialization_error.rb +4 -0
  9. data/lib/delayed/lifecycle.rb +84 -0
  10. data/lib/delayed/message_sending.rb +54 -0
  11. data/lib/delayed/performable_mailer.rb +21 -0
  12. data/lib/delayed/performable_method.rb +37 -0
  13. data/lib/delayed/plugin.rb +15 -0
  14. data/lib/delayed/plugins/clear_locks.rb +15 -0
  15. data/lib/delayed/psych_ext.rb +132 -0
  16. data/lib/delayed/railtie.rb +16 -0
  17. data/lib/delayed/recipes.rb +50 -0
  18. data/lib/delayed/serialization/active_record.rb +19 -0
  19. data/lib/delayed/syck_ext.rb +34 -0
  20. data/lib/delayed/tasks.rb +11 -0
  21. data/lib/delayed/worker.rb +242 -0
  22. data/lib/delayed/yaml_ext.rb +10 -0
  23. data/lib/delayed_job.rb +21 -0
  24. data/lib/generators/delayed_job/delayed_job_generator.rb +11 -0
  25. data/lib/generators/delayed_job/templates/script +5 -0
  26. data/recipes/delayed_job.rb +1 -0
  27. data/spec/autoloaded/clazz.rb +7 -0
  28. data/spec/autoloaded/instance_clazz.rb +6 -0
  29. data/spec/autoloaded/instance_struct.rb +6 -0
  30. data/spec/autoloaded/struct.rb +7 -0
  31. data/spec/delayed/backend/test.rb +113 -0
  32. data/spec/delayed/serialization/test.rb +0 -0
  33. data/spec/fixtures/bad_alias.yml +1 -0
  34. data/spec/lifecycle_spec.rb +67 -0
  35. data/spec/message_sending_spec.rb +113 -0
  36. data/spec/performable_mailer_spec.rb +44 -0
  37. data/spec/performable_method_spec.rb +89 -0
  38. data/spec/sample_jobs.rb +75 -0
  39. data/spec/spec_helper.rb +53 -0
  40. data/spec/test_backend_spec.rb +13 -0
  41. data/spec/worker_spec.rb +19 -0
  42. data/spec/yaml_ext_spec.rb +41 -0
  43. metadata +214 -0
@@ -0,0 +1,595 @@
1
+ require File.expand_path('../../../../spec/sample_jobs', __FILE__)
2
+
3
+ require 'active_support/core_ext'
4
+
5
+ shared_examples_for 'a delayed_job backend' do
6
+ let(:worker) { Delayed::Worker.new }
7
+
8
+ def create_job(opts = {})
9
+ described_class.create(opts.merge(:payload_object => SimpleJob.new))
10
+ end
11
+
12
+ before do
13
+ Delayed::Worker.max_priority = nil
14
+ Delayed::Worker.min_priority = nil
15
+ Delayed::Worker.default_priority = 99
16
+ Delayed::Worker.delay_jobs = true
17
+ SimpleJob.runs = 0
18
+ described_class.delete_all
19
+ end
20
+
21
+ it "should set run_at automatically if not set" do
22
+ described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
23
+ end
24
+
25
+ it "should not set run_at automatically if already set" do
26
+ later = described_class.db_time_now + 5.minutes
27
+ job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
28
+ job.run_at.should be_within(1).of(later)
29
+ end
30
+
31
+ describe "#reload" do
32
+ it 'should cause the payload to be reloaded' do
33
+ job = described_class.enqueue :payload_object => SimpleJob.new
34
+ job.payload_object.object_id.should_not == job.reload.payload_object.object_id
35
+ end
36
+ end
37
+
38
+ describe "enqueue" do
39
+ context "with a hash" do
40
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
41
+ lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
42
+ end
43
+
44
+ it "should be able to set priority" do
45
+ job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
46
+ job.priority.should == 5
47
+ end
48
+
49
+ it "should use default priority" do
50
+ job = described_class.enqueue :payload_object => SimpleJob.new
51
+ job.priority.should == 99
52
+ end
53
+
54
+ it "should be able to set run_at" do
55
+ later = described_class.db_time_now + 5.minutes
56
+ job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
57
+ job.run_at.should be_within(1).of(later)
58
+ end
59
+
60
+ it "should be able to set queue" do
61
+ job = described_class.enqueue :payload_object => SimpleJob.new, :queue => 'tracking'
62
+ job.queue.should == 'tracking'
63
+ end
64
+ end
65
+
66
+ context "with multiple arguments" do
67
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
68
+ lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
69
+ end
70
+
71
+ it "should increase count after enqueuing items" do
72
+ described_class.enqueue SimpleJob.new
73
+ described_class.count.should == 1
74
+ end
75
+
76
+ it "should be able to set priority [DEPRECATED]" do
77
+ silence_warnings do
78
+ job = described_class.enqueue SimpleJob.new, 5
79
+ job.priority.should == 5
80
+ end
81
+ end
82
+
83
+ it "should use default priority when it is not set" do
84
+ @job = described_class.enqueue SimpleJob.new
85
+ @job.priority.should == 99
86
+ end
87
+
88
+ it "should be able to set run_at [DEPRECATED]" do
89
+ silence_warnings do
90
+ later = described_class.db_time_now + 5.minutes
91
+ @job = described_class.enqueue SimpleJob.new, 5, later
92
+ @job.run_at.should be_within(1).of(later)
93
+ end
94
+ end
95
+
96
+ it "should work with jobs in modules" do
97
+ M::ModuleJob.runs = 0
98
+ job = described_class.enqueue M::ModuleJob.new
99
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
100
+ end
101
+ end
102
+
103
+ context "with delay_jobs = false" do
104
+ before(:each) do
105
+ Delayed::Worker.delay_jobs = false
106
+ end
107
+
108
+ it "should not increase count after enqueuing items" do
109
+ described_class.enqueue SimpleJob.new
110
+ described_class.count.should == 0
111
+ end
112
+
113
+ it 'should invoke the enqueued job' do
114
+ job = SimpleJob.new
115
+ job.should_receive(:perform)
116
+ described_class.enqueue job
117
+ end
118
+
119
+ it 'should return a job, not the result of invocation' do
120
+ described_class.enqueue(SimpleJob.new).should be_instance_of(described_class)
121
+ end
122
+ end
123
+ end
124
+
125
+ describe "callbacks" do
126
+ before(:each) do
127
+ CallbackJob.messages = []
128
+ end
129
+
130
+ %w(before success after).each do |callback|
131
+ it "should call #{callback} with job" do
132
+ job = described_class.enqueue(CallbackJob.new)
133
+ job.payload_object.should_receive(callback).with(job)
134
+ job.invoke_job
135
+ end
136
+ end
137
+
138
+ it "should call before and after callbacks" do
139
+ job = described_class.enqueue(CallbackJob.new)
140
+ CallbackJob.messages.should == ["enqueue"]
141
+ job.invoke_job
142
+ CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after"]
143
+ end
144
+
145
+ it "should call the after callback with an error" do
146
+ job = described_class.enqueue(CallbackJob.new)
147
+ job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
148
+
149
+ lambda { job.invoke_job }.should raise_error
150
+ CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
151
+ end
152
+
153
+ it "should call error when before raises an error" do
154
+ job = described_class.enqueue(CallbackJob.new)
155
+ job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
156
+ lambda { job.invoke_job }.should raise_error(RuntimeError)
157
+ CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
158
+ end
159
+ end
160
+
161
+ describe "payload_object" do
162
+ it "should raise a DeserializationError when the job class is totally unknown" do
163
+ job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
164
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
165
+ end
166
+
167
+ it "should raise a DeserializationError when the job struct is totally unknown" do
168
+ job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
169
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
170
+ end
171
+
172
+ it "should raise a DeserializationError when the YAML.load raises argument error" do
173
+ job = described_class.new :handler => "--- !ruby/struct:GoingToRaiseArgError {}"
174
+ YAML.should_receive(:load).and_raise(ArgumentError)
175
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
176
+ end
177
+ end
178
+
179
+ describe "reserve" do
180
+ before do
181
+ Delayed::Worker.max_run_time = 2.minutes
182
+ end
183
+
184
+ it "should not reserve failed jobs" do
185
+ create_job :attempts => 50, :failed_at => described_class.db_time_now
186
+ described_class.reserve(worker).should be_nil
187
+ end
188
+
189
+ it "should not reserve jobs scheduled for the future" do
190
+ create_job :run_at => described_class.db_time_now + 1.minute
191
+ described_class.reserve(worker).should be_nil
192
+ end
193
+
194
+ it "should reserve jobs scheduled for the past" do
195
+ job = create_job :run_at => described_class.db_time_now - 1.minute
196
+ described_class.reserve(worker).should == job
197
+ end
198
+
199
+ it "should reserve jobs scheduled for the past when time zones are involved" do
200
+ Time.zone = 'US/Eastern'
201
+ job = create_job :run_at => described_class.db_time_now - 1.minute
202
+ described_class.reserve(worker).should == job
203
+ end
204
+
205
+ it "should not reserve jobs locked by other workers" do
206
+ job = create_job
207
+ other_worker = Delayed::Worker.new
208
+ other_worker.name = 'other_worker'
209
+ described_class.reserve(other_worker).should == job
210
+ described_class.reserve(worker).should be_nil
211
+ end
212
+
213
+ it "should reserve open jobs" do
214
+ job = create_job
215
+ described_class.reserve(worker).should == job
216
+ end
217
+
218
+ it "should reserve expired jobs" do
219
+ job = create_job(:locked_by => 'some other worker', :locked_at => described_class.db_time_now - Delayed::Worker.max_run_time - 1.minute)
220
+ described_class.reserve(worker).should == job
221
+ end
222
+
223
+ it "should reserve own jobs" do
224
+ job = create_job(:locked_by => worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
225
+ described_class.reserve(worker).should == job
226
+ end
227
+ end
228
+
229
+ context "#name" do
230
+ it "should be the class name of the job that was enqueued" do
231
+ described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
232
+ end
233
+
234
+ it "should be the method that will be called if its a performable method object" do
235
+ job = described_class.new(:payload_object => NamedJob.new)
236
+ job.name.should == 'named_job'
237
+ end
238
+
239
+ it "should be the instance method that will be called if its a performable method object" do
240
+ job = Story.create(:text => "...").delay.save
241
+ job.name.should == 'Story#save'
242
+ end
243
+
244
+ it "should parse from handler on deserialization error" do
245
+ job = Story.create(:text => "...").delay.text
246
+ job.payload_object.object.destroy
247
+ job.reload.name.should == 'Delayed::PerformableMethod'
248
+ end
249
+ end
250
+
251
+ context "worker prioritization" do
252
+ before(:each) do
253
+ Delayed::Worker.max_priority = nil
254
+ Delayed::Worker.min_priority = nil
255
+ end
256
+
257
+ it "should fetch jobs ordered by priority" do
258
+ 10.times { described_class.enqueue SimpleJob.new, :priority => rand(10) }
259
+ jobs = []
260
+ 10.times { jobs << described_class.reserve(worker) }
261
+ jobs.size.should == 10
262
+ jobs.each_cons(2) do |a, b|
263
+ a.priority.should <= b.priority
264
+ end
265
+ end
266
+
267
+ it "should only find jobs greater than or equal to min priority" do
268
+ min = 5
269
+ Delayed::Worker.min_priority = min
270
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
271
+ 5.times { described_class.reserve(worker).priority.should >= min }
272
+ end
273
+
274
+ it "should only find jobs less than or equal to max priority" do
275
+ max = 5
276
+ Delayed::Worker.max_priority = max
277
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
278
+ 5.times { described_class.reserve(worker).priority.should <= max }
279
+ end
280
+ end
281
+
282
+ context "worker read-ahead" do
283
+ before do
284
+ @read_ahead = Delayed::Worker.read_ahead
285
+ end
286
+
287
+ after do
288
+ Delayed::Worker.read_ahead = @read_ahead
289
+ end
290
+
291
+ it "should read five jobs" do
292
+ described_class.should_receive(:find_available).with(anything, 5, anything).and_return([])
293
+ described_class.reserve(worker)
294
+ end
295
+
296
+ it "should read a configurable number of jobs" do
297
+ Delayed::Worker.read_ahead = 15
298
+ described_class.should_receive(:find_available).with(anything, Delayed::Worker.read_ahead, anything).and_return([])
299
+ described_class.reserve(worker)
300
+ end
301
+ end
302
+
303
+ context "clear_locks!" do
304
+ before do
305
+ @job = create_job(:locked_by => 'worker1', :locked_at => described_class.db_time_now)
306
+ end
307
+
308
+ it "should clear locks for the given worker" do
309
+ described_class.clear_locks!('worker1')
310
+ described_class.reserve(worker).should == @job
311
+ end
312
+
313
+ it "should not clear locks for other workers" do
314
+ described_class.clear_locks!('different_worker')
315
+ described_class.reserve(worker).should_not == @job
316
+ end
317
+ end
318
+
319
+ context "unlock" do
320
+ before do
321
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
322
+ end
323
+
324
+ it "should clear locks" do
325
+ @job.unlock
326
+ @job.locked_by.should be_nil
327
+ @job.locked_at.should be_nil
328
+ end
329
+ end
330
+
331
+ context "large handler" do
332
+ before do
333
+ text = "Lorem ipsum dolor sit amet. " * 1000
334
+ @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
335
+ end
336
+
337
+ it "should have an id" do
338
+ @job.id.should_not be_nil
339
+ end
340
+ end
341
+
342
+ context "named queues" do
343
+ context "when worker has one queue set" do
344
+ before(:each) do
345
+ worker.queues = ['large']
346
+ end
347
+
348
+ it "should only work off jobs which are from its queue" do
349
+ SimpleJob.runs.should == 0
350
+
351
+ create_job(:queue => "large")
352
+ create_job(:queue => "small")
353
+ worker.work_off
354
+
355
+ SimpleJob.runs.should == 1
356
+ end
357
+ end
358
+
359
+ context "when worker has two queue set" do
360
+ before(:each) do
361
+ worker.queues = ['large', 'small']
362
+ end
363
+
364
+ it "should only work off jobs which are from its queue" do
365
+ SimpleJob.runs.should == 0
366
+
367
+ create_job(:queue => "large")
368
+ create_job(:queue => "small")
369
+ create_job(:queue => "medium")
370
+ create_job
371
+ worker.work_off
372
+
373
+ SimpleJob.runs.should == 2
374
+ end
375
+ end
376
+
377
+ context "when worker does not have queue set" do
378
+ before(:each) do
379
+ worker.queues = []
380
+ end
381
+
382
+ it "should work off all jobs" do
383
+ SimpleJob.runs.should == 0
384
+
385
+ create_job(:queue => "one")
386
+ create_job(:queue => "two")
387
+ create_job
388
+ worker.work_off
389
+
390
+ SimpleJob.runs.should == 3
391
+ end
392
+ end
393
+ end
394
+
395
+ context "max_attempts" do
396
+ before(:each) do
397
+ @job = described_class.enqueue SimpleJob.new
398
+ end
399
+
400
+ it 'should not be defined' do
401
+ @job.max_attempts.should be_nil
402
+ end
403
+
404
+ it 'should use the max_retries value on the payload when defined' do
405
+ @job.payload_object.stub!(:max_attempts).and_return(99)
406
+ @job.max_attempts.should == 99
407
+ end
408
+ end
409
+
410
+ describe "yaml serialization" do
411
+ it "should reload changed attributes" do
412
+ story = Story.create(:text => 'hello')
413
+ job = story.delay.tell
414
+ story.update_attributes :text => 'goodbye'
415
+ job.reload.payload_object.object.text.should == 'goodbye'
416
+ end
417
+
418
+ it "should raise error ArgumentError the record is not persisted" do
419
+ story = Story.new(:text => 'hello')
420
+ lambda {
421
+ story.delay.tell
422
+ }.should raise_error(ArgumentError, "Jobs cannot be created for records before they've been persisted")
423
+
424
+ end
425
+
426
+ it "should raise deserialization error for destroyed records" do
427
+ story = Story.create(:text => 'hello')
428
+ job = story.delay.tell
429
+ story.destroy
430
+ lambda {
431
+ job.reload.payload_object
432
+ }.should raise_error(Delayed::DeserializationError)
433
+ end
434
+ end
435
+
436
+ describe "worker integration" do
437
+ before do
438
+ Delayed::Job.delete_all
439
+ SimpleJob.runs = 0
440
+ end
441
+
442
+ describe "running a job" do
443
+ it "should fail after Worker.max_run_time" do
444
+ begin
445
+ old_max_run_time = Delayed::Worker.max_run_time
446
+ Delayed::Worker.max_run_time = 1.second
447
+ job = Delayed::Job.create :payload_object => LongRunningJob.new
448
+ worker.run(job)
449
+ job.reload.last_error.should =~ /expired/
450
+ job.attempts.should == 1
451
+ ensure
452
+ Delayed::Worker.max_run_time = old_max_run_time
453
+ end
454
+ end
455
+
456
+ context "when the job raises a deserialization error" do
457
+ it "should mark the job as failed" do
458
+ Delayed::Worker.destroy_failed_jobs = false
459
+ job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
460
+ worker.work_off
461
+ job.reload
462
+ job.should be_failed
463
+ end
464
+ end
465
+ end
466
+
467
+ describe "failed jobs" do
468
+ before do
469
+ # reset defaults
470
+ Delayed::Worker.destroy_failed_jobs = true
471
+ Delayed::Worker.max_attempts = 25
472
+
473
+ @job = Delayed::Job.enqueue(ErrorJob.new)
474
+ end
475
+
476
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
477
+ Delayed::Worker.destroy_failed_jobs = false
478
+ Delayed::Worker.max_attempts = 1
479
+ worker.run(@job)
480
+ @job.reload
481
+ @job.last_error.should =~ /did not work/
482
+ @job.attempts.should == 1
483
+ @job.should be_failed
484
+ end
485
+
486
+ it "should re-schedule jobs after failing" do
487
+ worker.work_off
488
+ @job.reload
489
+ @job.last_error.should =~ /did not work/
490
+ @job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
491
+ @job.attempts.should == 1
492
+ @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
493
+ @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
494
+ @job.locked_by.should be_nil
495
+ @job.locked_at.should be_nil
496
+ end
497
+
498
+ it 'should re-schedule with handler provided time if present' do
499
+ job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
500
+ worker.run(job)
501
+ job.reload
502
+
503
+ (Delayed::Job.db_time_now + 99.minutes - job.run_at).abs.should < 1
504
+ end
505
+
506
+ it "should not fail when the triggered error doesn't have a message" do
507
+ error_with_nil_message = StandardError.new
508
+ error_with_nil_message.stub!(:message).and_return nil
509
+ @job.stub!(:invoke_job).and_raise error_with_nil_message
510
+ lambda{worker.run(@job)}.should_not raise_error
511
+ end
512
+ end
513
+
514
+ context "reschedule" do
515
+ before do
516
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
517
+ end
518
+
519
+ share_examples_for "any failure more than Worker.max_attempts times" do
520
+ context "when the job's payload has a #failure hook" do
521
+ before do
522
+ @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
523
+ @job.payload_object.should respond_to :failure
524
+ end
525
+
526
+ it "should run that hook" do
527
+ @job.payload_object.should_receive :failure
528
+ worker.reschedule(@job)
529
+ end
530
+ end
531
+
532
+ context "when the job's payload has no #failure hook" do
533
+ # It's a little tricky to test this in a straightforward way,
534
+ # because putting a should_not_receive expectation on
535
+ # @job.payload_object.failure makes that object
536
+ # incorrectly return true to
537
+ # payload_object.respond_to? :failure, which is what
538
+ # reschedule uses to decide whether to call failure.
539
+ # So instead, we just make sure that the payload_object as it
540
+ # already stands doesn't respond_to? failure, then
541
+ # shove it through the iterated reschedule loop and make sure we
542
+ # don't get a NoMethodError (caused by calling that nonexistent
543
+ # failure method).
544
+
545
+ before do
546
+ @job.payload_object.should_not respond_to(:failure)
547
+ end
548
+
549
+ it "should not try to run that hook" do
550
+ lambda do
551
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
552
+ end.should_not raise_exception(NoMethodError)
553
+ end
554
+ end
555
+ end
556
+
557
+ context "and we want to destroy jobs" do
558
+ before do
559
+ Delayed::Worker.destroy_failed_jobs = true
560
+ end
561
+
562
+ it_should_behave_like "any failure more than Worker.max_attempts times"
563
+
564
+ it "should be destroyed if it failed more than Worker.max_attempts times" do
565
+ @job.should_receive(:destroy)
566
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
567
+ end
568
+
569
+ it "should not be destroyed if failed fewer than Worker.max_attempts times" do
570
+ @job.should_not_receive(:destroy)
571
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
572
+ end
573
+ end
574
+
575
+ context "and we don't want to destroy jobs" do
576
+ before do
577
+ Delayed::Worker.destroy_failed_jobs = false
578
+ end
579
+
580
+ it_should_behave_like "any failure more than Worker.max_attempts times"
581
+
582
+ it "should be failed if it failed more than Worker.max_attempts times" do
583
+ @job.reload.should_not be_failed
584
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
585
+ @job.reload.should be_failed
586
+ end
587
+
588
+ it "should not be failed if it failed fewer than Worker.max_attempts times" do
589
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
590
+ @job.reload.should_not be_failed
591
+ end
592
+ end
593
+ end
594
+ end
595
+ end