drewda_delayed_job 3.0.3

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