delayed_job_hooked 2.1.5

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.
@@ -0,0 +1,485 @@
1
+ require File.expand_path('../../../../spec/sample_jobs', __FILE__)
2
+
3
+ shared_examples_for 'a delayed_job backend' do
4
+ let(:worker) { Delayed::Worker.new }
5
+
6
+ def create_job(opts = {})
7
+ described_class.create(opts.merge(:payload_object => SimpleJob.new))
8
+ end
9
+
10
+ before do
11
+ Delayed::Worker.max_priority = nil
12
+ Delayed::Worker.min_priority = nil
13
+ Delayed::Worker.default_priority = 99
14
+ Delayed::Worker.delay_jobs = true
15
+ SimpleJob.runs = 0
16
+ described_class.delete_all
17
+ end
18
+
19
+ it "should set run_at automatically if not set" do
20
+ described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
21
+ end
22
+
23
+ it "should not set run_at automatically if already set" do
24
+ later = described_class.db_time_now + 5.minutes
25
+ job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
26
+ job.run_at.should be_within(1).of(later)
27
+ end
28
+
29
+ describe "enqueue" do
30
+ context "with a hash" do
31
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
32
+ lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
33
+ end
34
+
35
+ it "should be able to set priority" do
36
+ job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
37
+ job.priority.should == 5
38
+ end
39
+
40
+ it "should use default priority" do
41
+ job = described_class.enqueue :payload_object => 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 :payload_object => SimpleJob.new, :run_at => later
48
+ job.run_at.should be_within(1).of(later)
49
+ end
50
+ end
51
+
52
+ context "with multiple arguments" do
53
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
54
+ lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
55
+ end
56
+
57
+ it "should increase count after enqueuing items" do
58
+ described_class.enqueue SimpleJob.new
59
+ described_class.count.should == 1
60
+ end
61
+
62
+ it "should not increase count after enqueuing items when delay_jobs is false" do
63
+ Delayed::Worker.delay_jobs = false
64
+ described_class.enqueue SimpleJob.new
65
+ described_class.count.should == 0
66
+ end
67
+
68
+ it "should be able to set priority [DEPRECATED]" do
69
+ silence_warnings do
70
+ job = described_class.enqueue SimpleJob.new, 5
71
+ job.priority.should == 5
72
+ end
73
+ end
74
+
75
+ it "should use default priority when it is not set" do
76
+ @job = described_class.enqueue SimpleJob.new
77
+ @job.priority.should == 99
78
+ end
79
+
80
+ it "should be able to set run_at [DEPRECATED]" do
81
+ silence_warnings do
82
+ later = described_class.db_time_now + 5.minutes
83
+ @job = described_class.enqueue SimpleJob.new, 5, later
84
+ @job.run_at.should be_within(1).of(later)
85
+ end
86
+ end
87
+
88
+ it "should work with jobs in modules" do
89
+ M::ModuleJob.runs = 0
90
+ job = described_class.enqueue M::ModuleJob.new
91
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "callbacks" do
97
+ before(:each) do
98
+ CallbackJob.messages = []
99
+ end
100
+
101
+ %w(before success after).each do |callback|
102
+ it "should call #{callback} with job" do
103
+ job = described_class.enqueue(CallbackJob.new)
104
+ job.payload_object.should_receive(callback).with(job)
105
+ job.invoke_job
106
+ end
107
+ end
108
+
109
+ it "should call before and after callbacks" do
110
+ job = described_class.enqueue(CallbackJob.new)
111
+ CallbackJob.messages.should == ["enqueue"]
112
+ job.invoke_job
113
+ CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after", "completed"]
114
+ end
115
+
116
+ it "should call the after callback with an error" do
117
+ job = described_class.enqueue(CallbackJob.new)
118
+ job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
119
+
120
+ lambda { job.invoke_job }.should raise_error
121
+ CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
122
+ end
123
+
124
+ it "should call error when before raises an error" do
125
+ job = described_class.enqueue(CallbackJob.new)
126
+ job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
127
+ lambda { job.invoke_job }.should raise_error(RuntimeError)
128
+ CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
129
+ end
130
+ end
131
+
132
+ describe "payload_object" do
133
+ it "should raise a DeserializationError when the job class is totally unknown" do
134
+ job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
135
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
136
+ end
137
+
138
+ it "should raise a DeserializationError when the job struct is totally unknown" do
139
+ job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
140
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
141
+ end
142
+
143
+ it "should raise a DeserializationError when the YAML.load raises argument error" do
144
+ job = described_class.find(create_job.id)
145
+ YAML.should_receive(:load).and_raise(ArgumentError)
146
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
147
+ end
148
+ end
149
+
150
+ describe "reserve" do
151
+ before do
152
+ Delayed::Worker.max_run_time = 2.minutes
153
+ end
154
+
155
+ it "should not reserve failed jobs" do
156
+ create_job :attempts => 50, :failed_at => described_class.db_time_now
157
+ described_class.reserve(worker).should be_nil
158
+ end
159
+
160
+ it "should not reserve jobs scheduled for the future" do
161
+ create_job :run_at => described_class.db_time_now + 1.minute
162
+ described_class.reserve(worker).should be_nil
163
+ end
164
+
165
+ it "should reserve jobs scheduled for the past" do
166
+ job = create_job :run_at => described_class.db_time_now - 1.minute
167
+ described_class.reserve(worker).should == job
168
+ end
169
+
170
+ it "should reserve jobs scheduled for the past when time zones are involved" do
171
+ Time.zone = 'US/Eastern'
172
+ job = create_job :run_at => described_class.db_time_now - 1.minute.ago.in_time_zone
173
+ described_class.reserve(worker).should == job
174
+ end
175
+
176
+ it "should not reserve jobs locked by other workers" do
177
+ job = create_job
178
+ other_worker = Delayed::Worker.new
179
+ other_worker.name = 'other_worker'
180
+ described_class.reserve(other_worker).should == job
181
+ described_class.reserve(worker).should be_nil
182
+ end
183
+
184
+ it "should reserve open jobs" do
185
+ job = create_job
186
+ described_class.reserve(worker).should == job
187
+ end
188
+
189
+ it "should reserve expired jobs" do
190
+ job = create_job(:locked_by => worker.name, :locked_at => described_class.db_time_now - 3.minutes)
191
+ described_class.reserve(worker).should == job
192
+ end
193
+
194
+ it "should reserve own jobs" do
195
+ job = create_job(:locked_by => worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
196
+ described_class.reserve(worker).should == job
197
+ end
198
+ end
199
+
200
+ context "#name" do
201
+ it "should be the class name of the job that was enqueued" do
202
+ described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
203
+ end
204
+
205
+ it "should be the method that will be called if its a performable method object" do
206
+ job = described_class.new(:payload_object => NamedJob.new)
207
+ job.name.should == 'named_job'
208
+ end
209
+
210
+ it "should be the instance method that will be called if its a performable method object" do
211
+ @job = Story.create(:text => "...").delay.save
212
+ @job.name.should == 'Story#save'
213
+ end
214
+
215
+ it "should parse from handler on deserialization error" do
216
+ job = Story.create(:text => "...").delay.text
217
+ job.payload_object.object.destroy
218
+ job = described_class.find(job.id)
219
+ job.name.should == 'Delayed::PerformableMethod'
220
+ end
221
+ end
222
+
223
+ context "worker prioritization" do
224
+ before(:each) do
225
+ Delayed::Worker.max_priority = nil
226
+ Delayed::Worker.min_priority = nil
227
+ end
228
+
229
+ it "should fetch jobs ordered by priority" do
230
+ 10.times { described_class.enqueue SimpleJob.new, :priority => rand(10) }
231
+ jobs = []
232
+ 10.times { jobs << described_class.reserve(worker) }
233
+ jobs.size.should == 10
234
+ jobs.each_cons(2) do |a, b|
235
+ a.priority.should <= b.priority
236
+ end
237
+ end
238
+
239
+ it "should only find jobs greater than or equal to min priority" do
240
+ min = 5
241
+ Delayed::Worker.min_priority = min
242
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
243
+ 5.times { described_class.reserve(worker).priority.should >= min }
244
+ end
245
+
246
+ it "should only find jobs less than or equal to max priority" do
247
+ max = 5
248
+ Delayed::Worker.max_priority = max
249
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
250
+ 5.times { described_class.reserve(worker).priority.should <= max }
251
+ end
252
+ end
253
+
254
+ context "clear_locks!" do
255
+ before do
256
+ @job = create_job(:locked_by => 'worker1', :locked_at => described_class.db_time_now)
257
+ end
258
+
259
+ it "should clear locks for the given worker" do
260
+ described_class.clear_locks!('worker1')
261
+ described_class.reserve(worker).should == @job
262
+ end
263
+
264
+ it "should not clear locks for other workers" do
265
+ described_class.clear_locks!('different_worker')
266
+ described_class.reserve(worker).should_not == @job
267
+ end
268
+ end
269
+
270
+ context "unlock" do
271
+ before do
272
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
273
+ end
274
+
275
+ it "should clear locks" do
276
+ @job.unlock
277
+ @job.locked_by.should be_nil
278
+ @job.locked_at.should be_nil
279
+ end
280
+ end
281
+
282
+ context "large handler" do
283
+ before do
284
+ text = "Lorem ipsum dolor sit amet. " * 1000
285
+ @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
286
+ end
287
+
288
+ it "should have an id" do
289
+ @job.id.should_not be_nil
290
+ end
291
+ end
292
+
293
+ context "max_attempts" do
294
+ before(:each) do
295
+ @job = described_class.enqueue SimpleJob.new
296
+ end
297
+
298
+ it 'should not be defined' do
299
+ @job.max_attempts.should be_nil
300
+ end
301
+
302
+ it 'should use the max_retries value on the payload when defined' do
303
+ @job.payload_object.stub!(:max_attempts).and_return(99)
304
+ @job.max_attempts.should == 99
305
+ end
306
+ end
307
+
308
+ describe "yaml serialization" do
309
+ it "should reload changed attributes" do
310
+ story = Story.create(:text => 'hello')
311
+ job = story.delay.tell
312
+ story.update_attributes :text => 'goodbye'
313
+ described_class.find(job.id).payload_object.object.text.should == 'goodbye'
314
+ end
315
+
316
+ it "should raise deserialization error for destroyed records" do
317
+ story = Story.create(:text => 'hello')
318
+ job = story.delay.tell
319
+ story.destroy
320
+ lambda {
321
+ described_class.find(job.id).payload_object
322
+ }.should raise_error(Delayed::DeserializationError)
323
+ end
324
+ end
325
+
326
+ describe "worker integration" do
327
+ before do
328
+ Delayed::Job.delete_all
329
+ SimpleJob.runs = 0
330
+ end
331
+
332
+ describe "running a job" do
333
+ it "should fail after Worker.max_run_time" do
334
+ begin
335
+ old_max_run_time = Delayed::Worker.max_run_time
336
+ Delayed::Worker.max_run_time = 1.second
337
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
338
+ worker.run(@job)
339
+ @job.reload.last_error.should =~ /expired/
340
+ @job.attempts.should == 1
341
+ ensure
342
+ Delayed::Worker.max_run_time = old_max_run_time
343
+ end
344
+ end
345
+
346
+ context "when the job raises a deserialization error" do
347
+ it "should mark the job as failed" do
348
+ Delayed::Worker.destroy_failed_jobs = false
349
+ job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
350
+ worker.work_off
351
+ job.reload
352
+ job.failed_at.should_not be_nil
353
+ end
354
+ end
355
+ end
356
+
357
+ describe "failed jobs" do
358
+ before do
359
+ # reset defaults
360
+ Delayed::Worker.destroy_failed_jobs = true
361
+ Delayed::Worker.max_attempts = 25
362
+
363
+ @job = Delayed::Job.enqueue(ErrorJob.new)
364
+ end
365
+
366
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
367
+ Delayed::Worker.destroy_failed_jobs = false
368
+ Delayed::Worker.max_attempts = 1
369
+ worker.run(@job)
370
+ @job.reload
371
+ @job.last_error.should =~ /did not work/
372
+ @job.attempts.should == 1
373
+ @job.failed_at.should_not be_nil
374
+ end
375
+
376
+ it "should re-schedule jobs after failing" do
377
+ worker.work_off
378
+ @job.reload
379
+ @job.last_error.should =~ /did not work/
380
+ @job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
381
+ @job.attempts.should == 1
382
+ @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
383
+ @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
384
+ @job.locked_by.should be_nil
385
+ @job.locked_at.should be_nil
386
+ end
387
+
388
+ it 'should re-schedule with handler provided time if present' do
389
+ @job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
390
+ worker.run(@job)
391
+ @job.reload
392
+
393
+ (Delayed::Job.db_time_now + 99.minutes - @job.run_at).abs.should < 1
394
+ end
395
+
396
+ it "should not fail when the triggered error doesn't have a message" do
397
+ error_with_nil_message = StandardError.new
398
+ error_with_nil_message.stub!(:message).and_return nil
399
+ @job.stub!(:invoke_job).and_raise error_with_nil_message
400
+ lambda{worker.run(@job)}.should_not raise_error
401
+ end
402
+ end
403
+
404
+ context "reschedule" do
405
+ before do
406
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
407
+ end
408
+
409
+ share_examples_for "any failure more than Worker.max_attempts times" do
410
+ context "when the job's payload has a #failure hook" do
411
+ before do
412
+ @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
413
+ @job.payload_object.should respond_to :failure
414
+ end
415
+
416
+ it "should run that hook" do
417
+ @job.payload_object.should_receive :failure
418
+ worker.reschedule(@job)
419
+ end
420
+ end
421
+
422
+ context "when the job's payload has no #failure hook" do
423
+ # It's a little tricky to test this in a straightforward way,
424
+ # because putting a should_not_receive expectation on
425
+ # @job.payload_object.failure makes that object
426
+ # incorrectly return true to
427
+ # payload_object.respond_to? :failure, which is what
428
+ # reschedule uses to decide whether to call failure.
429
+ # So instead, we just make sure that the payload_object as it
430
+ # already stands doesn't respond_to? failure, then
431
+ # shove it through the iterated reschedule loop and make sure we
432
+ # don't get a NoMethodError (caused by calling that nonexistent
433
+ # failure method).
434
+
435
+ before do
436
+ @job.payload_object.should_not respond_to(:failure)
437
+ end
438
+
439
+ it "should not try to run that hook" do
440
+ lambda do
441
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
442
+ end.should_not raise_exception(NoMethodError)
443
+ end
444
+ end
445
+ end
446
+
447
+ context "and we want to destroy jobs" do
448
+ before do
449
+ Delayed::Worker.destroy_failed_jobs = true
450
+ end
451
+
452
+ it_should_behave_like "any failure more than Worker.max_attempts times"
453
+
454
+ it "should be destroyed if it failed more than Worker.max_attempts times" do
455
+ @job.should_receive(:destroy)
456
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
457
+ end
458
+
459
+ it "should not be destroyed if failed fewer than Worker.max_attempts times" do
460
+ @job.should_not_receive(:destroy)
461
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
462
+ end
463
+ end
464
+
465
+ context "and we don't want to destroy jobs" do
466
+ before do
467
+ Delayed::Worker.destroy_failed_jobs = false
468
+ end
469
+
470
+ it_should_behave_like "any failure more than Worker.max_attempts times"
471
+
472
+ it "should be failed if it failed more than Worker.max_attempts times" do
473
+ @job.reload.failed_at.should == nil
474
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
475
+ @job.reload.failed_at.should_not == nil
476
+ end
477
+
478
+ it "should not be failed if it failed fewer than Worker.max_attempts times" do
479
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
480
+ @job.reload.failed_at.should == nil
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end